@chrischall/office-outlook-mcp 0.10.1 → 0.11.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for Outlook / Microsoft 365 — mail, calendar, contacts and tasks via natural language",
10
- "version": "0.10.1"
10
+ "version": "0.11.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Outlook (Microsoft 365)",
16
16
  "source": "./",
17
17
  "description": "MCP server for Outlook / Microsoft 365 — triage unread mail, reply, answer meeting invites and schedule meetings, plus folders, calendar, contacts and tasks, with confirmation before anything is sent",
18
- "version": "0.10.1",
18
+ "version": "0.11.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "office-outlook-mcp",
3
3
  "displayName": "Outlook (Microsoft 365)",
4
- "version": "0.10.1",
4
+ "version": "0.11.0",
5
5
  "description": "Outlook / Microsoft 365 for Claude — triage unread mail, reply, answer meeting invites and schedule meetings, plus folders, calendar, contacts and tasks, with confirmation before anything is sent, using your signed-in browser session",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/README.md CHANGED
@@ -134,14 +134,16 @@ Everything is optional — with nothing set, the server captures from the browse
134
134
  `outlook_list_categories`, `outlook_list_tasks`, `outlook_list_rules`
135
135
 
136
136
  **Write** (ask you to confirm first — see [Confirmations](#confirmations)) —
137
- `outlook_send_message`, `outlook_create_draft`, `outlook_mark_read`,
137
+ `outlook_send_message`, `outlook_create_draft`, `outlook_update_draft`, `outlook_send_draft`, `outlook_mark_read`,
138
138
  `outlook_update_message`, `outlook_move_message`, `outlook_create_event`,
139
139
  `outlook_update_event`, `outlook_respond_to_invite`, `outlook_delete_event`,
140
140
  `outlook_reply`, `outlook_create_rule`, `outlook_update_rule`,
141
- `outlook_delete_rule`, `outlook_set_auto_reply`.
141
+ `outlook_delete_rule`, `outlook_set_auto_reply`, `outlook_create_folder`,
142
+ `outlook_update_folder` (rename or move), `outlook_delete_folder`
143
+ (permanent: the folder does not go to Deleted Items).
142
144
  Some skip the confirmation because nothing reaches anyone else:
143
- `outlook_create_draft` (the draft stays in Drafts until you send it from
144
- Outlook), `outlook_reply` with `draftOnly: true` (the reply stays in Drafts),
145
+ `outlook_create_draft` (the draft stays in Drafts until you send it with
146
+ `outlook_send_draft` or from Outlook), `outlook_create_folder` (an empty folder is easily deleted), `outlook_reply` with `draftOnly: true` (the reply stays in Drafts),
145
147
  `outlook_respond_to_invite` with `sendResponse: false`, and
146
148
  `outlook_delete_event` on your own appointment with no attendees or on a
147
149
  meeting the organizer already cancelled (in each, only your calendar
package/dist/bundle.js CHANGED
@@ -10588,8 +10588,8 @@ var init_pt = __esm({
10588
10588
  case "not_multiple_of":
10589
10589
  return `N\xFAmero inv\xE1lido: deve ser m\xFAltiplo de ${issue2.divisor}`;
10590
10590
  case "unrecognized_keys": {
10591
- const plural2 = issue2.keys.length > 1 ? "s" : "";
10592
- return `Chave${plural2} inv\xE1lida${plural2}: ${joinValues(issue2.keys, ", ")}`;
10591
+ const plural3 = issue2.keys.length > 1 ? "s" : "";
10592
+ return `Chave${plural3} inv\xE1lida${plural3}: ${joinValues(issue2.keys, ", ")}`;
10593
10593
  }
10594
10594
  case "invalid_key":
10595
10595
  return `Entrada inv\xE1lida n${translateOriginWithArticle(issue2.origin, "definite")}`;
@@ -10739,8 +10739,8 @@ var init_pt_BR = __esm({
10739
10739
  case "not_multiple_of":
10740
10740
  return `N\xFAmero inv\xE1lido: deve ser m\xFAltiplo de ${issue2.divisor}`;
10741
10741
  case "unrecognized_keys": {
10742
- const plural2 = issue2.keys.length > 1 ? "s" : "";
10743
- return `Chave${plural2} inv\xE1lida${plural2}: ${joinValues(issue2.keys, ", ")}`;
10742
+ const plural3 = issue2.keys.length > 1 ? "s" : "";
10743
+ return `Chave${plural3} inv\xE1lida${plural3}: ${joinValues(issue2.keys, ", ")}`;
10744
10744
  }
10745
10745
  case "invalid_key":
10746
10746
  return `Entrada inv\xE1lida n${translateOriginWithArticle(issue2.origin, "definite")}`;
@@ -37164,11 +37164,11 @@ async function confirmWrite(ctx, options) {
37164
37164
  }
37165
37165
  const query = request?.query ? Object.fromEntries(Object.entries(request.query).filter(([, v]) => v !== void 0)) : void 0;
37166
37166
  const hasQuery = query !== void 0 && Object.keys(query).length > 0;
37167
- const shown = willSend !== void 0 ? willSend : request?.body !== void 0 ? request.body : payload;
37167
+ const shown3 = willSend !== void 0 ? willSend : request?.body !== void 0 ? request.body : payload;
37168
37168
  const preview = {
37169
37169
  ...summary === void 0 ? {} : { action: summary },
37170
37170
  ...request === void 0 ? {} : { method: request.method, path: request.path },
37171
- ...shown === void 0 ? {} : { willSend: shown },
37171
+ ...shown3 === void 0 ? {} : { willSend: shown3 },
37172
37172
  ...hasQuery ? { willSendQuery: query } : {},
37173
37173
  ...extra
37174
37174
  };
@@ -39293,7 +39293,7 @@ var init_version = __esm({
39293
39293
  "src/version.ts"() {
39294
39294
  "use strict";
39295
39295
  PACKAGE_NAME = "office-outlook-mcp";
39296
- VERSION = "0.10.1";
39296
+ VERSION = "0.11.0";
39297
39297
  }
39298
39298
  });
39299
39299
 
@@ -60041,20 +60041,20 @@ var require_ms = __commonJS({
60041
60041
  function fmtLong(ms) {
60042
60042
  var msAbs = Math.abs(ms);
60043
60043
  if (msAbs >= d) {
60044
- return plural2(ms, msAbs, d, "day");
60044
+ return plural3(ms, msAbs, d, "day");
60045
60045
  }
60046
60046
  if (msAbs >= h) {
60047
- return plural2(ms, msAbs, h, "hour");
60047
+ return plural3(ms, msAbs, h, "hour");
60048
60048
  }
60049
60049
  if (msAbs >= m) {
60050
- return plural2(ms, msAbs, m, "minute");
60050
+ return plural3(ms, msAbs, m, "minute");
60051
60051
  }
60052
60052
  if (msAbs >= s) {
60053
- return plural2(ms, msAbs, s, "second");
60053
+ return plural3(ms, msAbs, s, "second");
60054
60054
  }
60055
60055
  return ms + " ms";
60056
60056
  }
60057
- function plural2(ms, msAbs, n, name3) {
60057
+ function plural3(ms, msAbs, n, name3) {
60058
60058
  var isPlural = msAbs >= n * 1.5;
60059
60059
  return Math.round(ms / n) + " " + name3 + (isPlural ? "s" : "");
60060
60060
  }
@@ -68629,12 +68629,12 @@ var TITLE_FLOOR = 24;
68629
68629
  var cut = (s, n) => s.length > n ? `${s.slice(0, Math.max(n - 1, 0))}\u2026` : s;
68630
68630
  function briefLine(title, date5, status) {
68631
68631
  const raw = flat(title) ?? "(no subject)";
68632
- const shown = raw.startsWith("-- ") || raw.startsWith("\\") ? `\\${raw}` : raw;
68632
+ const shown3 = raw.startsWith("-- ") || raw.startsWith("\\") ? `\\${raw}` : raw;
68633
68633
  const sep2 = ` \u2014 ${date5} \u2014 `;
68634
68634
  const room = BRIEF_MAX - sep2.length;
68635
- if (shown.length + status.length <= room) return `${shown}${sep2}${status}`;
68636
- const titleRoom = Math.max(room - status.length, Math.min(shown.length, TITLE_FLOOR));
68637
- const t = cut(shown, titleRoom);
68635
+ if (shown3.length + status.length <= room) return `${shown3}${sep2}${status}`;
68636
+ const titleRoom = Math.max(room - status.length, Math.min(shown3.length, TITLE_FLOOR));
68637
+ const t = cut(shown3, titleRoom);
68638
68638
  return cut(`${t}${sep2}${cut(status, room - t.length)}`, BRIEF_MAX);
68639
68639
  }
68640
68640
  var BRIEF_MORE = "-- more";
@@ -69921,7 +69921,7 @@ function registerWriteTools(server, client2) {
69921
69921
  server.registerTool(
69922
69922
  "outlook_create_draft",
69923
69923
  {
69924
- description: "Create a draft message in the Drafts folder without sending it. Returns the created draft, which can be reviewed and sent from Outlook. It does not ask to confirm: a draft reaches nobody until it is sent from Outlook. Calling it twice leaves two drafts. " + TIMEOUT_SENTENCE,
69924
+ description: "Create a draft message in the Drafts folder without sending it. Returns the created draft, which can be changed with outlook_update_draft and sent with outlook_send_draft. It does not ask to confirm: a draft reaches nobody until it is sent. Calling it twice leaves two drafts. " + TIMEOUT_SENTENCE,
69925
69925
  // Self-only and additive, like outlook_reply with draftOnly: nothing is
69926
69926
  // overwritten or sent. Not idempotent: a repeat creates a second draft.
69927
69927
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
@@ -70054,8 +70054,8 @@ function registerWriteTools(server, client2) {
70054
70054
  }
70055
70055
  const echoed = await client2.write("PATCH", pathOf(id), payload);
70056
70056
  const after = echoed && typeof echoed === "object" ? echoed : await client2.get(`${pathOf(id)}?$select=IsRead,Flag,Categories`) ?? {};
70057
- const unchanged = unchangedFields(payload, after);
70058
- return unchanged.length ? { id, ok: false, error: `Outlook accepted the write but these did not change: ${unchanged.join(", ")}.` } : { id, ok: true };
70057
+ const unchanged2 = unchangedFields(payload, after);
70058
+ return unchanged2.length ? { id, ok: false, error: `Outlook accepted the write but these did not change: ${unchanged2.join(", ")}.` } : { id, ok: true };
70059
70059
  } catch (e) {
70060
70060
  if (isCredentialFailure(e)) throw e;
70061
70061
  return { id, ok: false, error: e instanceof Error ? e.message : String(e) };
@@ -70258,28 +70258,28 @@ function registerWriteTools(server, client2) {
70258
70258
  await client2.write("PATCH", path2, payload);
70259
70259
  const writtenZone = (payload.Start ?? payload.End)?.TimeZone ?? tz;
70260
70260
  const after = await readEvent(writtenZone);
70261
- const unchanged = [];
70262
- if (payload.Subject !== void 0 && after?.Subject !== payload.Subject) unchanged.push("Subject");
70261
+ const unchanged2 = [];
70262
+ if (payload.Subject !== void 0 && after?.Subject !== payload.Subject) unchanged2.push("Subject");
70263
70263
  for (const key of ["Start", "End"]) {
70264
70264
  const want = payload[key];
70265
- if (want && trimTime2(after?.[key]?.DateTime) !== trimTime2(want.DateTime)) unchanged.push(key);
70265
+ if (want && trimTime2(after?.[key]?.DateTime) !== trimTime2(want.DateTime)) unchanged2.push(key);
70266
70266
  }
70267
70267
  if (payload.Location !== void 0 && after?.Location?.DisplayName !== args.location) {
70268
- unchanged.push("Location");
70268
+ unchanged2.push("Location");
70269
70269
  }
70270
70270
  if (payload.Attendees !== void 0) {
70271
70271
  const want = payload.Attendees.map(lower).sort().join();
70272
70272
  const got = (after?.Attendees ?? []).map(lower).sort().join();
70273
- if (want !== got) unchanged.push("Attendees");
70273
+ if (want !== got) unchanged2.push("Attendees");
70274
70274
  }
70275
70275
  const joinUrl = joinUrlOf(after);
70276
- if (addTeams && !joinUrl) unchanged.push("Teams meeting");
70276
+ if (addTeams && !joinUrl) unchanged2.push("Teams meeting");
70277
70277
  return minifiedResult({
70278
- updated: unchanged.length === 0,
70278
+ updated: unchanged2.length === 0,
70279
70279
  Id: after?.Id ?? args.id,
70280
70280
  changed: Object.keys(payload),
70281
70281
  ...joinUrl ? { JoinUrl: joinUrl } : {},
70282
- ...unchanged.length ? { warning: `Outlook accepted the update but these did not change: ${unchanged.join(", ")}.` } : {}
70282
+ ...unchanged2.length ? { warning: `Outlook accepted the update but these did not change: ${unchanged2.join(", ")}.` } : {}
70283
70283
  });
70284
70284
  }
70285
70285
  );
@@ -71826,6 +71826,424 @@ function registerRuleTools(server, client2) {
71826
71826
  );
71827
71827
  }
71828
71828
 
71829
+ // src/tools/folders.ts
71830
+ init_zod();
71831
+ init_dist2();
71832
+ var ROOT = "msgfolderroot";
71833
+ var RULES_PATH2 = "/me/MailFolders/inbox/MessageRules";
71834
+ var folderPath = (id) => `/me/mailfolders/${encodeURIComponent(id)}`;
71835
+ var PROTECTED = /* @__PURE__ */ new Set([...WELL_KNOWN, ROOT]);
71836
+ var LIST_HINT = 'Pass a folder id from outlook_list_folders or a well-known name such as "inbox".';
71837
+ function refuseWellKnown(id, verb) {
71838
+ if (PROTECTED.has(id.toLowerCase())) {
71839
+ throw new McpToolError(`"${id}" is one of Outlook's own folders and cannot be ${verb}.`, {
71840
+ hint: "Pick a folder you created; list them with outlook_list_folders."
71841
+ });
71842
+ }
71843
+ }
71844
+ async function readFolder(client2, id) {
71845
+ try {
71846
+ return await client2.get(folderPath(id));
71847
+ } catch (e) {
71848
+ const status = errorStatusOf(e);
71849
+ if (status !== 400 && status !== 404) throw e;
71850
+ throw new McpToolError(`No mail folder matches "${id}".`, { hint: LIST_HINT });
71851
+ }
71852
+ }
71853
+ var shown = (f) => ({ ...compactFolder(f), Parent: f.ParentFolderId });
71854
+ var STILL_DELETING = "Outlook did not answer in time. A large folder keeps running its delete on the server for minutes after the request (about 75 messages a second), so it is probably still deleting. Check the folder's Total in outlook_list_folders: falling means it is working, gone means done. Do not resend the delete.";
71855
+ var plural2 = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
71856
+ var SUBTREE_CAP = 500;
71857
+ var CHILD_SELECT = "Id,TotalItemCount,UnreadItemCount,ChildFolderCount";
71858
+ async function subtree(client2, root) {
71859
+ const out = {
71860
+ total: root.TotalItemCount ?? 0,
71861
+ unread: root.UnreadItemCount ?? 0,
71862
+ subfolders: 0,
71863
+ ids: [root.Id],
71864
+ partial: false
71865
+ };
71866
+ const queue = (root.ChildFolderCount ?? 0) > 0 ? [{ id: root.Id, expected: root.ChildFolderCount ?? 0 }] : [];
71867
+ while (queue.length) {
71868
+ const { id: parent, expected } = queue.shift();
71869
+ if (out.subfolders >= SUBTREE_CAP) {
71870
+ out.partial = true;
71871
+ break;
71872
+ }
71873
+ let page;
71874
+ try {
71875
+ page = await client2.get(`${folderPath(parent)}/childfolders`, { query: { $select: CHILD_SELECT, $top: 100 } });
71876
+ } catch {
71877
+ out.partial = true;
71878
+ continue;
71879
+ }
71880
+ const kids = page.value ?? [];
71881
+ if (kids.length >= 100 || kids.length < expected) out.partial = true;
71882
+ for (const k of kids) {
71883
+ out.subfolders++;
71884
+ out.total += k.TotalItemCount ?? 0;
71885
+ out.unread += k.UnreadItemCount ?? 0;
71886
+ if (k.Id) {
71887
+ out.ids.push(k.Id);
71888
+ if ((k.ChildFolderCount ?? 0) > 0) queue.push({ id: k.Id, expected: k.ChildFolderCount ?? 0 });
71889
+ }
71890
+ }
71891
+ }
71892
+ return out;
71893
+ }
71894
+ function contents(t) {
71895
+ const atLeast = t.partial ? "at least " : "";
71896
+ const mail = `${atLeast}${plural2(t.total, "message")}${t.unread ? ` (${t.unread} unread)` : ""}`;
71897
+ return t.subfolders ? `${mail}, counting its ${atLeast}${plural2(t.subfolders, "subfolder")}` : mail;
71898
+ }
71899
+ async function rulesUsing(client2, ids) {
71900
+ const doomed = new Set(ids);
71901
+ try {
71902
+ const page = await client2.get(RULES_PATH2);
71903
+ return (page.value ?? []).filter((r) => doomed.has(r.Actions?.MoveToFolder ?? "") || doomed.has(r.Actions?.CopyToFolder ?? "")).map((r) => r.DisplayName ?? "(unnamed rule)");
71904
+ } catch {
71905
+ return [];
71906
+ }
71907
+ }
71908
+ function registerFolderTools(server, client2) {
71909
+ server.registerTool(
71910
+ "outlook_create_folder",
71911
+ {
71912
+ description: "Create a mail folder: at the top level of the mailbox by default, or inside `parent`. Nothing reaches anyone else and an empty folder is easily deleted, so this does not ask for confirmation. Point rules or moves at the returned Id. " + TIMEOUT_SENTENCE,
71913
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
71914
+ _meta: workMeta("change", { aliases: ["new folder", "add folder", "make folder"] }),
71915
+ inputSchema: external_exports.object({
71916
+ name: external_exports.string().min(1).describe("Folder name"),
71917
+ parent: external_exports.string().min(1).optional().describe('Folder id or well-known name (e.g. "inbox") to create it inside. Omit for the top level.')
71918
+ })
71919
+ },
71920
+ async ({ name: name3, parent }) => {
71921
+ const path2 = `${folderPath(parent ?? ROOT)}/childfolders`;
71922
+ let write;
71923
+ try {
71924
+ write = await writeOrUnknown2(() => client2.write("POST", path2, { DisplayName: name3 }), "outlook_list_folders");
71925
+ } catch (e) {
71926
+ const status = errorStatusOf(e);
71927
+ if (status === 409) {
71928
+ throw new McpToolError(`A folder named "${name3}" already exists there.`, {
71929
+ hint: "Use the existing one (see outlook_list_folders) or pick another name."
71930
+ });
71931
+ }
71932
+ if (parent !== void 0 && (status === 400 || status === 404)) {
71933
+ throw new McpToolError(`No mail folder matches "${parent}".`, { hint: LIST_HINT });
71934
+ }
71935
+ throw e;
71936
+ }
71937
+ if (!write.ok) return write.result;
71938
+ return minifiedResult({ created: true, ...shown(write.value ?? { DisplayName: name3 }) });
71939
+ }
71940
+ );
71941
+ server.registerTool(
71942
+ "outlook_update_folder",
71943
+ {
71944
+ description: "Rename a mail folder, move it under another folder, or both. Its mail and subfolders go with it, and rules that file into it keep working. Outlook's own folders (Inbox, Archive, \u2026) are refused. The result is verified by re-reading the folder. " + TIMEOUT_SENTENCE + " " + CONFIRM_FLOW_SENTENCE,
71945
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
71946
+ _meta: workMeta("change", { aliases: ["rename folder", "move folder"] }),
71947
+ inputSchema: external_exports.object({
71948
+ id: external_exports.string().min(1).describe("Folder Id from outlook_list_folders"),
71949
+ name: external_exports.string().min(1).optional().describe("New name"),
71950
+ parent: external_exports.string().min(1).optional().describe('Folder id or well-known name to move it into; "msgfolderroot" is the top level'),
71951
+ confirmToken: confirmTokenParam
71952
+ })
71953
+ },
71954
+ async ({ id, name: name3, parent, confirmToken }, ctx) => {
71955
+ if (name3 === void 0 && parent === void 0) {
71956
+ throw new McpToolError("Nothing to change on this folder.", { hint: "Pass `name`, `parent` or both." });
71957
+ }
71958
+ refuseWellKnown(id, "renamed or moved");
71959
+ if (parent !== void 0 && parent === id) {
71960
+ throw new McpToolError("A folder cannot be moved into itself.", { hint: "Pick a different `parent`." });
71961
+ }
71962
+ const current = await readFolder(client2, id);
71963
+ const destination = parent === void 0 ? void 0 : await folderId(client2, parent);
71964
+ const label = current.DisplayName ?? id;
71965
+ const changes = [
71966
+ ...name3 !== void 0 ? [`rename it to "${name3}"`] : [],
71967
+ ...parent !== void 0 ? [parent.toLowerCase() === ROOT ? "move it to the top level" : `move it into ${parent}`] : []
71968
+ ].join(" and ");
71969
+ const gate = await confirmWrite(ctx, {
71970
+ tool: "outlook_update_folder",
71971
+ action: "mail.folder_update",
71972
+ message: `Review and confirm: ${changes}.`,
71973
+ summary: `Folder "${label}": ${changes}.`,
71974
+ account: void 0,
71975
+ target: id,
71976
+ // A rename and a move are separate calls; preview both, as sent.
71977
+ payload: [
71978
+ ...name3 !== void 0 ? [{ method: "PATCH", path: folderPath(id), body: { DisplayName: name3 } }] : [],
71979
+ ...destination !== void 0 ? [{ method: "POST", path: `${folderPath(id)}/move`, body: { DestinationId: destination } }] : []
71980
+ ],
71981
+ confirmToken,
71982
+ preview: { current: shown(current) }
71983
+ });
71984
+ if (gate) return gate;
71985
+ if (name3 !== void 0) {
71986
+ const rename3 = await writeOrUnknown2(() => client2.write("PATCH", folderPath(id), { DisplayName: name3 }), "outlook_list_folders");
71987
+ if (!rename3.ok) return rename3.result;
71988
+ }
71989
+ let finalId = id;
71990
+ if (destination !== void 0) {
71991
+ const move = await writeOrUnknown2(
71992
+ () => client2.write("POST", `${folderPath(id)}/move`, { DestinationId: destination }),
71993
+ "outlook_list_folders"
71994
+ );
71995
+ if (!move.ok) return move.result;
71996
+ finalId = move.value?.Id ?? id;
71997
+ }
71998
+ const read = await readBack(() => client2.get(folderPath(finalId)));
71999
+ if (!read.ok) return minifiedResult({ updated: true, Id: finalId, ...unverified("outlook_list_folders") });
72000
+ const after = read.value;
72001
+ const stale = [
72002
+ ...name3 !== void 0 && after.DisplayName !== name3 ? ["name"] : [],
72003
+ ...destination !== void 0 && after.ParentFolderId !== destination ? ["location"] : []
72004
+ ];
72005
+ return minifiedResult({
72006
+ updated: stale.length === 0,
72007
+ ...shown(after),
72008
+ ...stale.length ? { warning: `Outlook accepted the write but the ${stale.join(" and ")} did not change.` } : {}
72009
+ });
72010
+ }
72011
+ );
72012
+ server.registerTool(
72013
+ "outlook_delete_folder",
72014
+ {
72015
+ description: `Delete a mail folder together with every message and subfolder in it. This is permanent: the folder does NOT go to Deleted Items, so it cannot be restored from there. To keep the mail, move it out first (outlook_move_message). The confirmation totals the mail in the folder and every subfolder beneath it, and names any inbox rules that file into them (those rules stop working). Outlook's own folders are refused. The result is verified by re-reading the folder. A folder holding thousands of messages takes minutes to delete, so the call can come back with status "unknown" while Outlook keeps deleting: check its Total in outlook_list_folders falling, and never resend. ` + CONFIRM_FLOW_SENTENCE,
72016
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
72017
+ _meta: workMeta("change", { aliases: ["remove folder"] }),
72018
+ inputSchema: external_exports.object({
72019
+ id: external_exports.string().min(1).describe("Folder Id from outlook_list_folders"),
72020
+ confirmToken: confirmTokenParam
72021
+ })
72022
+ },
72023
+ async ({ id, confirmToken }, ctx) => {
72024
+ refuseWellKnown(id, "deleted");
72025
+ const current = await readFolder(client2, id);
72026
+ const label = current.DisplayName ?? id;
72027
+ const tree = await subtree(client2, { ...current, Id: current.Id ?? id });
72028
+ const rules = await rulesUsing(client2, tree.ids);
72029
+ const ruleNote = rules.length ? ` These inbox rules file mail into it or its subfolders and will stop working: ${rules.map((r) => `"${r}"`).join(", ")}.` : "";
72030
+ const gate = await confirmWrite(ctx, {
72031
+ tool: "outlook_delete_folder",
72032
+ action: "mail.folder_delete",
72033
+ message: `Review and confirm permanently deleting this folder with its ${contents(tree)} (it does not go to Deleted Items):${ruleNote}`,
72034
+ summary: `Permanently delete folder "${label}" with its ${contents(tree)}; it does not go to Deleted Items.${ruleNote}`,
72035
+ account: void 0,
72036
+ target: id,
72037
+ request: { method: "DELETE", path: folderPath(id) },
72038
+ confirmToken,
72039
+ preview: { current: shown(current), ...rules.length ? { rulesUsingIt: rules } : {} }
72040
+ });
72041
+ if (gate) return gate;
72042
+ let write;
72043
+ try {
72044
+ write = await writeOrUnknown2(() => client2.write("DELETE", folderPath(id)), "outlook_list_folders");
72045
+ } catch (e) {
72046
+ if (errorStatusOf(e) === 400 && /ErrorDeleteDistinguishedFolder/.test(String(e.message))) {
72047
+ throw new McpToolError(`"${label}" is one of Outlook's own folders and cannot be deleted.`, {
72048
+ hint: "Nothing was deleted. Leave it in place, or delete the folders you created inside it instead."
72049
+ });
72050
+ }
72051
+ throw e;
72052
+ }
72053
+ if (!write.ok) return minifiedResult({ status: "unknown", Id: id, warning: STILL_DELETING });
72054
+ const read = await readBack(() => client2.get(folderPath(id)));
72055
+ if (!read.ok && errorStatusOf(read.error) === 404) return minifiedResult({ deleted: true, Id: id });
72056
+ if (!read.ok) return minifiedResult({ deleted: true, Id: id, ...unverified("outlook_list_folders") });
72057
+ return minifiedResult({ deleted: false, Id: id, warning: "Outlook accepted the delete but the folder is still there." });
72058
+ }
72059
+ );
72060
+ }
72061
+
72062
+ // src/tools/drafts.ts
72063
+ init_zod();
72064
+ init_dist2();
72065
+ var DRAFT_SELECT = "Id,IsDraft,Subject,Body,ToRecipients,CcRecipients,BccRecipients,WebLink";
72066
+ var messagePath = (id) => `/me/messages/${encodeURIComponent(id)}`;
72067
+ var PREVIEW_BODY_MAX = 2e3;
72068
+ var addresses2 = (list) => (list ?? []).map((r) => r.EmailAddress?.Address ?? "");
72069
+ var sameAddresses = (a, b) => addresses2(a).map((x) => x.toLowerCase()).sort().join("\n") === addresses2(b).map((x) => x.toLowerCase()).sort().join("\n");
72070
+ var sameText = (a, b) => (a ?? "").replace(/\r\n/g, "\n").trim() === (b ?? "").replace(/\r\n/g, "\n").trim();
72071
+ function shown2(d) {
72072
+ return {
72073
+ Id: d.Id,
72074
+ Subject: d.Subject,
72075
+ To: addresses2(d.ToRecipients),
72076
+ Cc: addresses2(d.CcRecipients),
72077
+ Bcc: addresses2(d.BccRecipients),
72078
+ WebLink: d.WebLink
72079
+ };
72080
+ }
72081
+ function currentBody(d) {
72082
+ const content = d.Body?.Content ?? "";
72083
+ return content.length > PREVIEW_BODY_MAX ? `${content.slice(0, PREVIEW_BODY_MAX)}\u2026 (${content.length} characters in all)` : content;
72084
+ }
72085
+ function unchanged(payload, after) {
72086
+ const out = [];
72087
+ if (payload.Subject !== void 0 && after.Subject !== payload.Subject) out.push("Subject");
72088
+ if (payload.Body) {
72089
+ const ok = payload.Body.ContentType === "HTML" ? after.Body?.ContentType === "HTML" : after.Body?.ContentType === "Text" && sameText(after.Body?.Content, payload.Body.Content);
72090
+ if (!ok) out.push("Body");
72091
+ }
72092
+ if (payload.ToRecipients && !sameAddresses(after.ToRecipients, payload.ToRecipients)) out.push("To");
72093
+ if (payload.CcRecipients && !sameAddresses(after.CcRecipients, payload.CcRecipients)) out.push("Cc");
72094
+ if (payload.BccRecipients && !sameAddresses(after.BccRecipients, payload.BccRecipients)) out.push("Bcc");
72095
+ return out;
72096
+ }
72097
+ async function readDraft(client2, id) {
72098
+ let found;
72099
+ try {
72100
+ found = await client2.get(`${messagePath(id)}?$select=${DRAFT_SELECT}`);
72101
+ } catch (e) {
72102
+ const status = errorStatusOf(e);
72103
+ if (status !== 400 && status !== 404) throw e;
72104
+ throw new McpToolError(`No message matches "${id}".`, {
72105
+ hint: 'Pass a draft Id from outlook_create_draft, outlook_reply with draftOnly, or outlook_list_messages with folder "drafts".'
72106
+ });
72107
+ }
72108
+ if (found.IsDraft !== true) {
72109
+ throw new McpToolError(`"${found.Subject ?? id}" is not a draft, so it cannot be edited.`, {
72110
+ hint: "Only unsent drafts can be changed. To send something different, create a new draft or use outlook_send_message."
72111
+ });
72112
+ }
72113
+ return found;
72114
+ }
72115
+ var SEND_CHECK_TRIES = 8;
72116
+ function registerDraftTools(server, client2, opts = {}) {
72117
+ const delayMs = opts.sendCheckDelayMs ?? 1500;
72118
+ server.registerTool(
72119
+ "outlook_update_draft",
72120
+ {
72121
+ description: "Edit an unsent draft: its subject, body, and To, Cc or Bcc recipients. Only what you pass changes, and each field you pass replaces the current one: `body` replaces the WHOLE body (in a reply or forward draft that includes the quoted original), and `to: []` clears the To line. Nothing is sent; send it with outlook_send_draft. Messages that are not drafts are refused. The confirmation shows the draft as it is now, because the replaced text cannot be recovered. The result is checked against the draft Outlook returns. " + TIMEOUT_SENTENCE + " " + CONFIRM_FLOW_SENTENCE,
72122
+ // Overwrites text that is not kept anywhere else, so it is destructive
72123
+ // even though it reaches nobody. Repeating the same edit changes nothing.
72124
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
72125
+ _meta: workMeta("change", { aliases: ["edit draft", "change draft", "rewrite draft"] }),
72126
+ inputSchema: external_exports.object({
72127
+ id: external_exports.string().min(1).describe('Draft Id (from outlook_create_draft, outlook_reply draftOnly, or outlook_list_messages folder "drafts")'),
72128
+ subject: external_exports.string().optional().describe("New subject line"),
72129
+ body: external_exports.string().optional().describe("New body; replaces the whole current body"),
72130
+ html: external_exports.boolean().optional().describe("Treat `body` as HTML (default: plain text). Only with `body`."),
72131
+ to: recipientList.describe("New To recipients; replaces the list ([] clears it)"),
72132
+ cc: recipientList.describe("New Cc recipients; replaces the list ([] clears it)"),
72133
+ bcc: recipientList.describe("New Bcc recipients; replaces the list ([] clears it)"),
72134
+ confirmToken: confirmTokenParam
72135
+ })
72136
+ },
72137
+ async ({ id, subject, body, html, to, cc, bcc, confirmToken }, ctx) => {
72138
+ if (html !== void 0 && body === void 0) {
72139
+ throw new McpToolError("`html` only applies together with `body`.", { hint: "Pass the new `body` as well." });
72140
+ }
72141
+ const payload = {
72142
+ ...subject !== void 0 ? { Subject: subject } : {},
72143
+ ...body !== void 0 ? { Body: { ContentType: html === true ? "HTML" : "Text", Content: body } } : {},
72144
+ ...to !== void 0 ? { ToRecipients: toRecipients(to) } : {},
72145
+ ...cc !== void 0 ? { CcRecipients: toRecipients(cc) } : {},
72146
+ ...bcc !== void 0 ? { BccRecipients: toRecipients(bcc) } : {}
72147
+ };
72148
+ if (Object.keys(payload).length === 0) {
72149
+ throw new McpToolError("Nothing to change on this draft.", {
72150
+ hint: "Pass at least one of subject, body, to, cc or bcc."
72151
+ });
72152
+ }
72153
+ const current = await readDraft(client2, id);
72154
+ const fields = [
72155
+ ...subject !== void 0 ? ["subject"] : [],
72156
+ ...body !== void 0 ? ["whole body"] : [],
72157
+ ...to !== void 0 ? ["To list"] : [],
72158
+ ...cc !== void 0 ? ["Cc list"] : [],
72159
+ ...bcc !== void 0 ? ["Bcc list"] : []
72160
+ ];
72161
+ const path2 = messagePath(id);
72162
+ const gate = await confirmWrite(ctx, {
72163
+ tool: "outlook_update_draft",
72164
+ action: "mail.draft_update",
72165
+ message: `Review and confirm this draft edit. It will replace the draft's ${fields.join(", ")}; the current draft is shown below. Nothing is sent.`,
72166
+ summary: `Edit draft "${current.Subject ?? id}": replace its ${fields.join(", ")}. Nothing is sent.`,
72167
+ account: void 0,
72168
+ target: id,
72169
+ request: { method: "PATCH", path: path2, body: payload },
72170
+ confirmToken,
72171
+ preview: { current: { ...shown2(current), Body: currentBody(current) } }
72172
+ });
72173
+ if (gate) return gate;
72174
+ const write = await writeOrUnknown2(() => client2.write("PATCH", path2, payload), "Drafts");
72175
+ if (!write.ok) return write.result;
72176
+ const echoed = write.value;
72177
+ let after;
72178
+ if (echoed && typeof echoed === "object" && echoed.Id) {
72179
+ after = echoed;
72180
+ } else {
72181
+ const read = await readBack(() => client2.get(`${path2}?$select=${DRAFT_SELECT}`));
72182
+ if (!read.ok) return minifiedResult({ updated: true, Id: id, ...unverified("the draft in Drafts") });
72183
+ after = read.value;
72184
+ }
72185
+ const stale = unchanged(payload, after);
72186
+ return minifiedResult({
72187
+ updated: stale.length === 0,
72188
+ ...shown2(after),
72189
+ ...stale.length ? { warning: `Outlook accepted the edit but these did not change: ${stale.join(", ")}.` } : {}
72190
+ });
72191
+ }
72192
+ );
72193
+ server.registerTool(
72194
+ "outlook_send_draft",
72195
+ {
72196
+ description: "Send an existing draft as it stands: its subject, body, recipients and attachments go out unchanged. To change it first, use outlook_update_draft. Refuses a message that is not a draft, or a draft with no recipients. The confirmation shows exactly what will be sent and to whom. Once sent, Outlook files the copy in Sent Items under a new id. Outlook finishes a send in the background, so the result waits a few seconds for the draft to leave Drafts; if it has not, the result says to check Sent Items, never to resend. " + TIMEOUT_SENTENCE + " " + CONFIRM_FLOW_SENTENCE + " " + CONFIRM_INJECTION_RULE,
72197
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
72198
+ _meta: workMeta("send", { aliases: ["send draft", "send the draft"] }),
72199
+ inputSchema: external_exports.object({
72200
+ id: external_exports.string().min(1).describe('Draft Id (from outlook_create_draft, outlook_reply draftOnly, or outlook_list_messages folder "drafts")'),
72201
+ confirmToken: confirmTokenParam
72202
+ })
72203
+ },
72204
+ async ({ id, confirmToken }, ctx) => {
72205
+ const current = await readDraft(client2, id);
72206
+ const view = shown2(current);
72207
+ const recipients = [...view.To, ...view.Cc, ...view.Bcc];
72208
+ if (recipients.length === 0) {
72209
+ throw new McpToolError(`Draft "${current.Subject ?? id}" has no recipients, so there is no one to send it to.`, {
72210
+ hint: "Add To, Cc or Bcc recipients with outlook_update_draft, then send it."
72211
+ });
72212
+ }
72213
+ const path2 = `${messagePath(id)}/send`;
72214
+ const gate = await confirmWrite(ctx, {
72215
+ tool: "outlook_send_draft",
72216
+ action: "mail.send_draft",
72217
+ message: "Review and confirm this email before it is sent:",
72218
+ summary: `Send draft "${current.Subject ?? ""}" to ${recipients.join(", ")}`,
72219
+ account: void 0,
72220
+ target: id,
72221
+ request: { method: "POST", path: path2 },
72222
+ // The send carries no body: what goes out is the stored draft, so that is what is shown and bound.
72223
+ willSend: { Subject: current.Subject, To: view.To, Cc: view.Cc, Bcc: view.Bcc, Body: currentBody(current) },
72224
+ confirmToken
72225
+ });
72226
+ if (gate) return gate;
72227
+ const write = await writeOrUnknown2(() => client2.write("POST", path2), "Sent Items");
72228
+ if (!write.ok) return write.result;
72229
+ const summary = { subject: current.Subject, recipients };
72230
+ for (let attempt = 1; attempt <= SEND_CHECK_TRIES; attempt++) {
72231
+ const read = await readBack(() => client2.get(`${messagePath(id)}?$select=Id,IsDraft`));
72232
+ if (!read.ok && errorStatusOf(read.error) === 404) return minifiedResult({ sent: true, ...summary });
72233
+ if (!read.ok) return minifiedResult({ sent: true, ...summary, ...unverified("Sent Items") });
72234
+ if (read.value.IsDraft !== true) return minifiedResult({ sent: true, ...summary });
72235
+ if (attempt < SEND_CHECK_TRIES) await new Promise((r) => setTimeout(r, delayMs));
72236
+ }
72237
+ return minifiedResult({
72238
+ sent: true,
72239
+ verified: false,
72240
+ ...summary,
72241
+ warning: `Outlook accepted the send, but the draft had not left Drafts after ${Math.round(delayMs * (SEND_CHECK_TRIES - 1) / 1e3)} s; Outlook is probably still delivering it. Check Sent Items before doing anything else, and do not send it again unless it is missing there.`
72242
+ });
72243
+ }
72244
+ );
72245
+ }
72246
+
71829
72247
  // src/tools/copilot.ts
71830
72248
  init_zod();
71831
72249
  init_dist2();
@@ -72349,6 +72767,8 @@ await runMcp({
72349
72767
  registerWriteTools,
72350
72768
  registerExportTools,
72351
72769
  registerRuleTools,
72770
+ registerFolderTools,
72771
+ registerDraftTools,
72352
72772
  (server) => registerCopilotTools(server, copilot, client),
72353
72773
  registerHealthcheckTool
72354
72774
  ]
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@ import { registerWriteTools } from './tools/writes.js';
9
9
  import { registerHealthcheckTool } from './tools/healthcheck.js';
10
10
  import { registerExportTools } from './tools/export.js';
11
11
  import { registerRuleTools } from './tools/rules.js';
12
+ import { registerFolderTools } from './tools/folders.js';
13
+ import { registerDraftTools } from './tools/drafts.js';
12
14
  import { registerCopilotTools } from './tools/copilot.js';
13
15
  import { CopilotClient } from './copilot/client.js';
14
16
  loadDotenvSafely();
@@ -45,6 +47,8 @@ await runMcp({
45
47
  registerWriteTools,
46
48
  registerExportTools,
47
49
  registerRuleTools,
50
+ registerFolderTools,
51
+ registerDraftTools,
48
52
  (server) => registerCopilotTools(server, copilot, client),
49
53
  registerHealthcheckTool,
50
54
  ],
@@ -0,0 +1,219 @@
1
+ import { z } from 'zod';
2
+ import { CONFIRM_FLOW_SENTENCE, CONFIRM_INJECTION_RULE, confirmTokenParam, confirmWrite, errorStatusOf, McpToolError, minifiedResult, } from '@chrischall/mcp-utils';
3
+ import { workMeta } from './_hints.js';
4
+ import { readBack, unverified } from './rules.js';
5
+ import { recipientList, TIMEOUT_SENTENCE, toRecipients, writeOrUnknown } from './writes.js';
6
+ const DRAFT_SELECT = 'Id,IsDraft,Subject,Body,ToRecipients,CcRecipients,BccRecipients,WebLink';
7
+ const messagePath = (id) => `/me/messages/${encodeURIComponent(id)}`;
8
+ /** The preview shows the body being replaced; past this it is cut, and says so. */
9
+ const PREVIEW_BODY_MAX = 2000;
10
+ const addresses = (list) => (list ?? []).map((r) => r.EmailAddress?.Address ?? '');
11
+ const sameAddresses = (a, b) => addresses(a).map((x) => x.toLowerCase()).sort().join('\n') === addresses(b).map((x) => x.toLowerCase()).sort().join('\n');
12
+ /** Outlook may store line breaks as CRLF; the words are what must match. */
13
+ const sameText = (a, b) => (a ?? '').replace(/\r\n/g, '\n').trim() === (b ?? '').replace(/\r\n/g, '\n').trim();
14
+ function shown(d) {
15
+ return {
16
+ Id: d.Id,
17
+ Subject: d.Subject,
18
+ To: addresses(d.ToRecipients),
19
+ Cc: addresses(d.CcRecipients),
20
+ Bcc: addresses(d.BccRecipients),
21
+ WebLink: d.WebLink,
22
+ };
23
+ }
24
+ function currentBody(d) {
25
+ const content = d.Body?.Content ?? '';
26
+ return content.length > PREVIEW_BODY_MAX ? `${content.slice(0, PREVIEW_BODY_MAX)}… (${content.length} characters in all)` : content;
27
+ }
28
+ /** Fields the PATCH set that the draft Outlook returned does not show. */
29
+ function unchanged(payload, after) {
30
+ const out = [];
31
+ if (payload.Subject !== undefined && after.Subject !== payload.Subject)
32
+ out.push('Subject');
33
+ if (payload.Body) {
34
+ // Outlook wraps an HTML body in its own <html><head>…, so only its type can be checked.
35
+ const ok = payload.Body.ContentType === 'HTML'
36
+ ? after.Body?.ContentType === 'HTML'
37
+ : after.Body?.ContentType === 'Text' && sameText(after.Body?.Content, payload.Body.Content);
38
+ if (!ok)
39
+ out.push('Body');
40
+ }
41
+ if (payload.ToRecipients && !sameAddresses(after.ToRecipients, payload.ToRecipients))
42
+ out.push('To');
43
+ if (payload.CcRecipients && !sameAddresses(after.CcRecipients, payload.CcRecipients))
44
+ out.push('Cc');
45
+ if (payload.BccRecipients && !sameAddresses(after.BccRecipients, payload.BccRecipients))
46
+ out.push('Bcc');
47
+ return out;
48
+ }
49
+ async function readDraft(client, id) {
50
+ let found;
51
+ try {
52
+ found = await client.get(`${messagePath(id)}?$select=${DRAFT_SELECT}`);
53
+ }
54
+ catch (e) {
55
+ const status = errorStatusOf(e);
56
+ if (status !== 400 && status !== 404)
57
+ throw e;
58
+ throw new McpToolError(`No message matches "${id}".`, {
59
+ hint: 'Pass a draft Id from outlook_create_draft, outlook_reply with draftOnly, or outlook_list_messages with folder "drafts".',
60
+ });
61
+ }
62
+ if (found.IsDraft !== true) {
63
+ throw new McpToolError(`"${found.Subject ?? id}" is not a draft, so it cannot be edited.`, {
64
+ hint: 'Only unsent drafts can be changed. To send something different, create a new draft or use outlook_send_message.',
65
+ });
66
+ }
67
+ return found;
68
+ }
69
+ /**
70
+ * How long to watch for a sent draft to leave Drafts. Outlook answers the send
71
+ * with 202 and delivers in the background: live 2026-11-10 the draft was still
72
+ * readable as a draft straight after the 202 although the mail arrived.
73
+ */
74
+ const SEND_CHECK_TRIES = 8;
75
+ export function registerDraftTools(server, client, opts = {}) {
76
+ const delayMs = opts.sendCheckDelayMs ?? 1500;
77
+ server.registerTool('outlook_update_draft', {
78
+ description: 'Edit an unsent draft: its subject, body, and To, Cc or Bcc recipients. Only what you pass changes, and each field you pass replaces the current one: `body` replaces the WHOLE body (in a reply or forward draft that includes the quoted original), and `to: []` clears the To line. Nothing is sent; send it with outlook_send_draft. Messages that are not drafts are refused. The confirmation shows the draft as it is now, because the replaced text cannot be recovered. The result is checked against the draft Outlook returns. ' +
79
+ TIMEOUT_SENTENCE +
80
+ ' ' +
81
+ CONFIRM_FLOW_SENTENCE,
82
+ // Overwrites text that is not kept anywhere else, so it is destructive
83
+ // even though it reaches nobody. Repeating the same edit changes nothing.
84
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
85
+ _meta: workMeta('change', { aliases: ['edit draft', 'change draft', 'rewrite draft'] }),
86
+ inputSchema: z.object({
87
+ id: z.string().min(1).describe('Draft Id (from outlook_create_draft, outlook_reply draftOnly, or outlook_list_messages folder "drafts")'),
88
+ subject: z.string().optional().describe('New subject line'),
89
+ body: z.string().optional().describe('New body; replaces the whole current body'),
90
+ html: z.boolean().optional().describe('Treat `body` as HTML (default: plain text). Only with `body`.'),
91
+ to: recipientList.describe('New To recipients; replaces the list ([] clears it)'),
92
+ cc: recipientList.describe('New Cc recipients; replaces the list ([] clears it)'),
93
+ bcc: recipientList.describe('New Bcc recipients; replaces the list ([] clears it)'),
94
+ confirmToken: confirmTokenParam,
95
+ }),
96
+ }, async ({ id, subject, body, html, to, cc, bcc, confirmToken }, ctx) => {
97
+ if (html !== undefined && body === undefined) {
98
+ throw new McpToolError('`html` only applies together with `body`.', { hint: 'Pass the new `body` as well.' });
99
+ }
100
+ const payload = {
101
+ ...(subject !== undefined ? { Subject: subject } : {}),
102
+ ...(body !== undefined ? { Body: { ContentType: html === true ? 'HTML' : 'Text', Content: body } } : {}),
103
+ ...(to !== undefined ? { ToRecipients: toRecipients(to) } : {}),
104
+ ...(cc !== undefined ? { CcRecipients: toRecipients(cc) } : {}),
105
+ ...(bcc !== undefined ? { BccRecipients: toRecipients(bcc) } : {}),
106
+ };
107
+ if (Object.keys(payload).length === 0) {
108
+ throw new McpToolError('Nothing to change on this draft.', {
109
+ hint: 'Pass at least one of subject, body, to, cc or bcc.',
110
+ });
111
+ }
112
+ const current = await readDraft(client, id);
113
+ const fields = [
114
+ ...(subject !== undefined ? ['subject'] : []),
115
+ ...(body !== undefined ? ['whole body'] : []),
116
+ ...(to !== undefined ? ['To list'] : []),
117
+ ...(cc !== undefined ? ['Cc list'] : []),
118
+ ...(bcc !== undefined ? ['Bcc list'] : []),
119
+ ];
120
+ const path = messagePath(id);
121
+ const gate = await confirmWrite(ctx, {
122
+ tool: 'outlook_update_draft',
123
+ action: 'mail.draft_update',
124
+ message: `Review and confirm this draft edit. It will replace the draft's ${fields.join(', ')}; the current draft is shown below. Nothing is sent.`,
125
+ summary: `Edit draft "${current.Subject ?? id}": replace its ${fields.join(', ')}. Nothing is sent.`,
126
+ account: undefined,
127
+ target: id,
128
+ request: { method: 'PATCH', path, body: payload },
129
+ confirmToken,
130
+ preview: { current: { ...shown(current), Body: currentBody(current) } },
131
+ });
132
+ if (gate)
133
+ return gate;
134
+ const write = await writeOrUnknown(() => client.write('PATCH', path, payload), 'Drafts');
135
+ if (!write.ok)
136
+ return write.result;
137
+ // Outlook answers the PATCH with the updated draft; re-read only when it did not.
138
+ const echoed = write.value;
139
+ let after;
140
+ if (echoed && typeof echoed === 'object' && echoed.Id) {
141
+ after = echoed;
142
+ }
143
+ else {
144
+ const read = await readBack(() => client.get(`${path}?$select=${DRAFT_SELECT}`));
145
+ if (!read.ok)
146
+ return minifiedResult({ updated: true, Id: id, ...unverified('the draft in Drafts') });
147
+ after = read.value;
148
+ }
149
+ const stale = unchanged(payload, after);
150
+ return minifiedResult({
151
+ updated: stale.length === 0,
152
+ ...shown(after),
153
+ ...(stale.length ? { warning: `Outlook accepted the edit but these did not change: ${stale.join(', ')}.` } : {}),
154
+ });
155
+ });
156
+ server.registerTool('outlook_send_draft', {
157
+ description: 'Send an existing draft as it stands: its subject, body, recipients and attachments go out unchanged. To change it first, use outlook_update_draft. Refuses a message that is not a draft, or a draft with no recipients. The confirmation shows exactly what will be sent and to whom. Once sent, Outlook files the copy in Sent Items under a new id. Outlook finishes a send in the background, so the result waits a few seconds for the draft to leave Drafts; if it has not, the result says to check Sent Items, never to resend. ' +
158
+ TIMEOUT_SENTENCE +
159
+ ' ' +
160
+ CONFIRM_FLOW_SENTENCE +
161
+ ' ' +
162
+ CONFIRM_INJECTION_RULE,
163
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
164
+ _meta: workMeta('send', { aliases: ['send draft', 'send the draft'] }),
165
+ inputSchema: z.object({
166
+ id: z.string().min(1).describe('Draft Id (from outlook_create_draft, outlook_reply draftOnly, or outlook_list_messages folder "drafts")'),
167
+ confirmToken: confirmTokenParam,
168
+ }),
169
+ }, async ({ id, confirmToken }, ctx) => {
170
+ const current = await readDraft(client, id);
171
+ const view = shown(current);
172
+ const recipients = [...view.To, ...view.Cc, ...view.Bcc];
173
+ if (recipients.length === 0) {
174
+ throw new McpToolError(`Draft "${current.Subject ?? id}" has no recipients, so there is no one to send it to.`, {
175
+ hint: 'Add To, Cc or Bcc recipients with outlook_update_draft, then send it.',
176
+ });
177
+ }
178
+ const path = `${messagePath(id)}/send`;
179
+ const gate = await confirmWrite(ctx, {
180
+ tool: 'outlook_send_draft',
181
+ action: 'mail.send_draft',
182
+ message: 'Review and confirm this email before it is sent:',
183
+ summary: `Send draft "${current.Subject ?? ''}" to ${recipients.join(', ')}`,
184
+ account: undefined,
185
+ target: id,
186
+ request: { method: 'POST', path },
187
+ // The send carries no body: what goes out is the stored draft, so that is what is shown and bound.
188
+ willSend: { Subject: current.Subject, To: view.To, Cc: view.Cc, Bcc: view.Bcc, Body: currentBody(current) },
189
+ confirmToken,
190
+ });
191
+ if (gate)
192
+ return gate;
193
+ const write = await writeOrUnknown(() => client.write('POST', path), 'Sent Items');
194
+ if (!write.ok)
195
+ return write.result;
196
+ const summary = { subject: current.Subject, recipients };
197
+ // Sending moves the draft out of Drafts, so its old id stops resolving,
198
+ // but only once Outlook has finished: watch for that for a few seconds.
199
+ // Outlook accepted the send, so nothing here ever reports "not sent": a
200
+ // resend on that word would mail everyone twice.
201
+ for (let attempt = 1; attempt <= SEND_CHECK_TRIES; attempt++) {
202
+ const read = await readBack(() => client.get(`${messagePath(id)}?$select=Id,IsDraft`));
203
+ if (!read.ok && errorStatusOf(read.error) === 404)
204
+ return minifiedResult({ sent: true, ...summary });
205
+ if (!read.ok)
206
+ return minifiedResult({ sent: true, ...summary, ...unverified('Sent Items') });
207
+ if (read.value.IsDraft !== true)
208
+ return minifiedResult({ sent: true, ...summary });
209
+ if (attempt < SEND_CHECK_TRIES)
210
+ await new Promise((r) => setTimeout(r, delayMs));
211
+ }
212
+ return minifiedResult({
213
+ sent: true,
214
+ verified: false,
215
+ ...summary,
216
+ warning: `Outlook accepted the send, but the draft had not left Drafts after ${Math.round((delayMs * (SEND_CHECK_TRIES - 1)) / 1000)} s; Outlook is probably still delivering it. Check Sent Items before doing anything else, and do not send it again unless it is missing there.`,
217
+ });
218
+ });
219
+ }
@@ -0,0 +1,279 @@
1
+ import { z } from 'zod';
2
+ import { CONFIRM_FLOW_SENTENCE, confirmTokenParam, confirmWrite, errorStatusOf, McpToolError, minifiedResult, } from '@chrischall/mcp-utils';
3
+ import { compactFolder } from '../view.js';
4
+ import { workMeta } from './_hints.js';
5
+ import { WELL_KNOWN } from './mail.js';
6
+ import { folderId, readBack, unverified } from './rules.js';
7
+ import { TIMEOUT_SENTENCE, writeOrUnknown } from './writes.js';
8
+ /** The mailbox's top level: the parent of Inbox, Archive and the folders beside them. */
9
+ const ROOT = 'msgfolderroot';
10
+ const RULES_PATH = '/me/MailFolders/inbox/MessageRules';
11
+ const folderPath = (id) => `/me/mailfolders/${encodeURIComponent(id)}`;
12
+ const PROTECTED = new Set([...WELL_KNOWN, ROOT]);
13
+ const LIST_HINT = 'Pass a folder id from outlook_list_folders or a well-known name such as "inbox".';
14
+ /** Outlook's own folders cannot be renamed, moved or deleted, so refuse them by name before any request. */
15
+ function refuseWellKnown(id, verb) {
16
+ if (PROTECTED.has(id.toLowerCase())) {
17
+ throw new McpToolError(`"${id}" is one of Outlook's own folders and cannot be ${verb}.`, {
18
+ hint: 'Pick a folder you created; list them with outlook_list_folders.',
19
+ });
20
+ }
21
+ }
22
+ async function readFolder(client, id) {
23
+ try {
24
+ return await client.get(folderPath(id));
25
+ }
26
+ catch (e) {
27
+ const status = errorStatusOf(e);
28
+ if (status !== 400 && status !== 404)
29
+ throw e;
30
+ throw new McpToolError(`No mail folder matches "${id}".`, { hint: LIST_HINT });
31
+ }
32
+ }
33
+ const shown = (f) => ({ ...compactFolder(f), Parent: f.ParentFolderId });
34
+ /**
35
+ * Outlook deletes a big folder's mail after the DELETE is accepted, at about 75
36
+ * messages a second (live 2026-11-10: 13,370 took ~3 minutes, 63,466 ~14), so
37
+ * the call outlives the client timeout. A resend while it runs would only
38
+ * restart a delete that is already happening.
39
+ */
40
+ const STILL_DELETING = 'Outlook did not answer in time. A large folder keeps running its delete on the server for minutes after the request (about 75 messages a second), so it is probably still deleting. Check the folder\'s Total in outlook_list_folders: falling means it is working, gone means done. Do not resend the delete.';
41
+ const plural = (n, one) => `${n} ${one}${n === 1 ? '' : 's'}`;
42
+ /** Past this many folders the walk stops and the counts read "at least". */
43
+ const SUBTREE_CAP = 500;
44
+ const CHILD_SELECT = 'Id,TotalItemCount,UnreadItemCount,ChildFolderCount';
45
+ /**
46
+ * A folder's own TotalItemCount leaves out its subfolders, and a delete takes
47
+ * them all: live 2026-11-10 a parent read "0 messages" over 2,725. So the
48
+ * confirmation walks the tree. A failed or capped walk is reported as a lower
49
+ * bound rather than blocking the delete.
50
+ */
51
+ async function subtree(client, root) {
52
+ const out = {
53
+ total: root.TotalItemCount ?? 0,
54
+ unread: root.UnreadItemCount ?? 0,
55
+ subfolders: 0,
56
+ ids: [root.Id],
57
+ partial: false,
58
+ };
59
+ const queue = (root.ChildFolderCount ?? 0) > 0 ? [{ id: root.Id, expected: root.ChildFolderCount ?? 0 }] : [];
60
+ while (queue.length) {
61
+ const { id: parent, expected } = queue.shift();
62
+ if (out.subfolders >= SUBTREE_CAP) {
63
+ out.partial = true;
64
+ break;
65
+ }
66
+ let page;
67
+ try {
68
+ page = await client.get(`${folderPath(parent)}/childfolders`, { query: { $select: CHILD_SELECT, $top: 100 } });
69
+ }
70
+ catch {
71
+ out.partial = true;
72
+ continue;
73
+ }
74
+ const kids = page.value ?? [];
75
+ // Hidden subfolders count in ChildFolderCount but are not listed (live: Conversation History).
76
+ if (kids.length >= 100 || kids.length < expected)
77
+ out.partial = true;
78
+ for (const k of kids) {
79
+ out.subfolders++;
80
+ out.total += k.TotalItemCount ?? 0;
81
+ out.unread += k.UnreadItemCount ?? 0;
82
+ if (k.Id) {
83
+ out.ids.push(k.Id);
84
+ if ((k.ChildFolderCount ?? 0) > 0)
85
+ queue.push({ id: k.Id, expected: k.ChildFolderCount ?? 0 });
86
+ }
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+ /** What goes with a folder when it is deleted, in words. */
92
+ function contents(t) {
93
+ const atLeast = t.partial ? 'at least ' : '';
94
+ const mail = `${atLeast}${plural(t.total, 'message')}${t.unread ? ` (${t.unread} unread)` : ''}`;
95
+ return t.subfolders ? `${mail}, counting its ${atLeast}${plural(t.subfolders, 'subfolder')}` : mail;
96
+ }
97
+ /**
98
+ * Rules that file mail into this folder or any folder beneath it. Losing the folder breaks them, so the
99
+ * confirmation names them. A failed read only loses the warning, not the delete.
100
+ */
101
+ async function rulesUsing(client, ids) {
102
+ const doomed = new Set(ids);
103
+ try {
104
+ const page = await client.get(RULES_PATH);
105
+ return (page.value ?? [])
106
+ .filter((r) => doomed.has(r.Actions?.MoveToFolder ?? '') || doomed.has(r.Actions?.CopyToFolder ?? ''))
107
+ .map((r) => r.DisplayName ?? '(unnamed rule)');
108
+ }
109
+ catch {
110
+ return [];
111
+ }
112
+ }
113
+ export function registerFolderTools(server, client) {
114
+ server.registerTool('outlook_create_folder', {
115
+ description: 'Create a mail folder: at the top level of the mailbox by default, or inside `parent`. Nothing reaches anyone else and an empty folder is easily deleted, so this does not ask for confirmation. Point rules or moves at the returned Id. ' +
116
+ TIMEOUT_SENTENCE,
117
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
118
+ _meta: workMeta('change', { aliases: ['new folder', 'add folder', 'make folder'] }),
119
+ inputSchema: z.object({
120
+ name: z.string().min(1).describe('Folder name'),
121
+ parent: z
122
+ .string()
123
+ .min(1)
124
+ .optional()
125
+ .describe('Folder id or well-known name (e.g. "inbox") to create it inside. Omit for the top level.'),
126
+ }),
127
+ }, async ({ name, parent }) => {
128
+ const path = `${folderPath(parent ?? ROOT)}/childfolders`;
129
+ let write;
130
+ try {
131
+ write = await writeOrUnknown(() => client.write('POST', path, { DisplayName: name }), 'outlook_list_folders');
132
+ }
133
+ catch (e) {
134
+ const status = errorStatusOf(e);
135
+ if (status === 409) {
136
+ throw new McpToolError(`A folder named "${name}" already exists there.`, {
137
+ hint: 'Use the existing one (see outlook_list_folders) or pick another name.',
138
+ });
139
+ }
140
+ if (parent !== undefined && (status === 400 || status === 404)) {
141
+ throw new McpToolError(`No mail folder matches "${parent}".`, { hint: LIST_HINT });
142
+ }
143
+ throw e;
144
+ }
145
+ if (!write.ok)
146
+ return write.result;
147
+ return minifiedResult({ created: true, ...shown(write.value ?? { DisplayName: name }) });
148
+ });
149
+ server.registerTool('outlook_update_folder', {
150
+ description: 'Rename a mail folder, move it under another folder, or both. Its mail and subfolders go with it, and rules that file into it keep working. Outlook\'s own folders (Inbox, Archive, …) are refused. The result is verified by re-reading the folder. ' +
151
+ TIMEOUT_SENTENCE +
152
+ ' ' +
153
+ CONFIRM_FLOW_SENTENCE,
154
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
155
+ _meta: workMeta('change', { aliases: ['rename folder', 'move folder'] }),
156
+ inputSchema: z.object({
157
+ id: z.string().min(1).describe('Folder Id from outlook_list_folders'),
158
+ name: z.string().min(1).optional().describe('New name'),
159
+ parent: z
160
+ .string()
161
+ .min(1)
162
+ .optional()
163
+ .describe('Folder id or well-known name to move it into; "msgfolderroot" is the top level'),
164
+ confirmToken: confirmTokenParam,
165
+ }),
166
+ }, async ({ id, name, parent, confirmToken }, ctx) => {
167
+ if (name === undefined && parent === undefined) {
168
+ throw new McpToolError('Nothing to change on this folder.', { hint: 'Pass `name`, `parent` or both.' });
169
+ }
170
+ refuseWellKnown(id, 'renamed or moved');
171
+ if (parent !== undefined && parent === id) {
172
+ throw new McpToolError('A folder cannot be moved into itself.', { hint: 'Pick a different `parent`.' });
173
+ }
174
+ const current = await readFolder(client, id);
175
+ const destination = parent === undefined ? undefined : await folderId(client, parent);
176
+ const label = current.DisplayName ?? id;
177
+ const changes = [
178
+ ...(name !== undefined ? [`rename it to "${name}"`] : []),
179
+ ...(parent !== undefined ? [parent.toLowerCase() === ROOT ? 'move it to the top level' : `move it into ${parent}`] : []),
180
+ ].join(' and ');
181
+ const gate = await confirmWrite(ctx, {
182
+ tool: 'outlook_update_folder',
183
+ action: 'mail.folder_update',
184
+ message: `Review and confirm: ${changes}.`,
185
+ summary: `Folder "${label}": ${changes}.`,
186
+ account: undefined,
187
+ target: id,
188
+ // A rename and a move are separate calls; preview both, as sent.
189
+ payload: [
190
+ ...(name !== undefined ? [{ method: 'PATCH', path: folderPath(id), body: { DisplayName: name } }] : []),
191
+ ...(destination !== undefined
192
+ ? [{ method: 'POST', path: `${folderPath(id)}/move`, body: { DestinationId: destination } }]
193
+ : []),
194
+ ],
195
+ confirmToken,
196
+ preview: { current: shown(current) },
197
+ });
198
+ if (gate)
199
+ return gate;
200
+ if (name !== undefined) {
201
+ const rename = await writeOrUnknown(() => client.write('PATCH', folderPath(id), { DisplayName: name }), 'outlook_list_folders');
202
+ if (!rename.ok)
203
+ return rename.result;
204
+ }
205
+ let finalId = id;
206
+ if (destination !== undefined) {
207
+ const move = await writeOrUnknown(() => client.write('POST', `${folderPath(id)}/move`, { DestinationId: destination }), 'outlook_list_folders');
208
+ if (!move.ok)
209
+ return move.result;
210
+ finalId = move.value?.Id ?? id;
211
+ }
212
+ const read = await readBack(() => client.get(folderPath(finalId)));
213
+ if (!read.ok)
214
+ return minifiedResult({ updated: true, Id: finalId, ...unverified('outlook_list_folders') });
215
+ const after = read.value;
216
+ const stale = [
217
+ ...(name !== undefined && after.DisplayName !== name ? ['name'] : []),
218
+ ...(destination !== undefined && after.ParentFolderId !== destination ? ['location'] : []),
219
+ ];
220
+ return minifiedResult({
221
+ updated: stale.length === 0,
222
+ ...shown(after),
223
+ ...(stale.length ? { warning: `Outlook accepted the write but the ${stale.join(' and ')} did not change.` } : {}),
224
+ });
225
+ });
226
+ server.registerTool('outlook_delete_folder', {
227
+ description: 'Delete a mail folder together with every message and subfolder in it. This is permanent: the folder does NOT go to Deleted Items, so it cannot be restored from there. To keep the mail, move it out first (outlook_move_message). The confirmation totals the mail in the folder and every subfolder beneath it, and names any inbox rules that file into them (those rules stop working). Outlook\'s own folders are refused. The result is verified by re-reading the folder. A folder holding thousands of messages takes minutes to delete, so the call can come back with status "unknown" while Outlook keeps deleting: check its Total in outlook_list_folders falling, and never resend. ' +
228
+ CONFIRM_FLOW_SENTENCE,
229
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
230
+ _meta: workMeta('change', { aliases: ['remove folder'] }),
231
+ inputSchema: z.object({
232
+ id: z.string().min(1).describe('Folder Id from outlook_list_folders'),
233
+ confirmToken: confirmTokenParam,
234
+ }),
235
+ }, async ({ id, confirmToken }, ctx) => {
236
+ refuseWellKnown(id, 'deleted');
237
+ const current = await readFolder(client, id);
238
+ const label = current.DisplayName ?? id;
239
+ const tree = await subtree(client, { ...current, Id: current.Id ?? id });
240
+ const rules = await rulesUsing(client, tree.ids);
241
+ const ruleNote = rules.length
242
+ ? ` These inbox rules file mail into it or its subfolders and will stop working: ${rules.map((r) => `"${r}"`).join(', ')}.`
243
+ : '';
244
+ const gate = await confirmWrite(ctx, {
245
+ tool: 'outlook_delete_folder',
246
+ action: 'mail.folder_delete',
247
+ message: `Review and confirm permanently deleting this folder with its ${contents(tree)} (it does not go to Deleted Items):${ruleNote}`,
248
+ summary: `Permanently delete folder "${label}" with its ${contents(tree)}; it does not go to Deleted Items.${ruleNote}`,
249
+ account: undefined,
250
+ target: id,
251
+ request: { method: 'DELETE', path: folderPath(id) },
252
+ confirmToken,
253
+ preview: { current: shown(current), ...(rules.length ? { rulesUsingIt: rules } : {}) },
254
+ });
255
+ if (gate)
256
+ return gate;
257
+ let write;
258
+ try {
259
+ write = await writeOrUnknown(() => client.write('DELETE', folderPath(id)), 'outlook_list_folders');
260
+ }
261
+ catch (e) {
262
+ // Built-in folders such as RSS Feeds are not all reachable by a well-known name, so Outlook is the backstop.
263
+ if (errorStatusOf(e) === 400 && /ErrorDeleteDistinguishedFolder/.test(String(e.message))) {
264
+ throw new McpToolError(`"${label}" is one of Outlook's own folders and cannot be deleted.`, {
265
+ hint: 'Nothing was deleted. Leave it in place, or delete the folders you created inside it instead.',
266
+ });
267
+ }
268
+ throw e;
269
+ }
270
+ if (!write.ok)
271
+ return minifiedResult({ status: 'unknown', Id: id, warning: STILL_DELETING });
272
+ const read = await readBack(() => client.get(folderPath(id)));
273
+ if (!read.ok && errorStatusOf(read.error) === 404)
274
+ return minifiedResult({ deleted: true, Id: id });
275
+ if (!read.ok)
276
+ return minifiedResult({ deleted: true, Id: id, ...unverified('outlook_list_folders') });
277
+ return minifiedResult({ deleted: false, Id: id, warning: 'Outlook accepted the delete but the folder is still there.' });
278
+ });
279
+ }
@@ -13,7 +13,7 @@ import { mailboxUntrusted, UNTRUSTED_DESCRIPTION_SUFFIX } from './_untrusted.js'
13
13
  * Well-known folder names the API accepts wherever a folder id is taken.
14
14
  * All eight verified present on a live mailbox 2026-09-20.
15
15
  */
16
- const WELL_KNOWN = [
16
+ export const WELL_KNOWN = [
17
17
  'inbox',
18
18
  'drafts',
19
19
  'sentitems',
@@ -56,7 +56,7 @@ function outlookConditions(c) {
56
56
  * first. That also refuses a folder that does not exist before anyone is asked
57
57
  * to confirm a rule that could never work.
58
58
  */
59
- async function folderId(client, folder) {
59
+ export async function folderId(client, folder) {
60
60
  if (folder === undefined)
61
61
  return undefined;
62
62
  try {
@@ -114,7 +114,7 @@ function endsBeforeStart(start, end) {
114
114
  * The re-read after a write that Outlook accepted. If only the re-read fails,
115
115
  * the write still happened, so an error here would invite a duplicate.
116
116
  */
117
- async function readBack(read) {
117
+ export async function readBack(read) {
118
118
  try {
119
119
  return { ok: true, value: await read() };
120
120
  }
@@ -122,7 +122,7 @@ async function readBack(read) {
122
122
  return { ok: false, error };
123
123
  }
124
124
  }
125
- const unverified = (where) => ({
125
+ export const unverified = (where) => ({
126
126
  verified: false,
127
127
  warning: `Outlook accepted this change but it could not be re-read. Check ${where} before retrying.`,
128
128
  });
@@ -6,11 +6,11 @@ import { mailboxTimeZone } from '../timezone.js';
6
6
  import { cappedRecipients, MAX_RECIPIENTS } from '../view.js';
7
7
  import { EVENT_EXPAND, isMeetingMessage, MEETING_MESSAGE_TYPE, resolvedEvent, } from './_invite.js';
8
8
  import { mailboxUntrusted, UNTRUSTED_DESCRIPTION_SUFFIX } from './_untrusted.js';
9
- const recipientList = z
9
+ export const recipientList = z
10
10
  .array(z.string().min(3))
11
11
  .optional()
12
12
  .describe('Email addresses');
13
- function toRecipients(list) {
13
+ export function toRecipients(list) {
14
14
  return (list ?? []).map((Address) => ({ EmailAddress: { Address } }));
15
15
  }
16
16
  /**
@@ -242,8 +242,8 @@ export function registerWriteTools(server, client) {
242
242
  return minifiedResult({ sent: true, subject, recipients: recipients || null });
243
243
  });
244
244
  server.registerTool('outlook_create_draft', {
245
- description: 'Create a draft message in the Drafts folder without sending it. Returns the created draft, which can be reviewed and sent from Outlook. ' +
246
- 'It does not ask to confirm: a draft reaches nobody until it is sent from Outlook. Calling it twice leaves two drafts. ' +
245
+ description: 'Create a draft message in the Drafts folder without sending it. Returns the created draft, which can be changed with outlook_update_draft and sent with outlook_send_draft. ' +
246
+ 'It does not ask to confirm: a draft reaches nobody until it is sent. Calling it twice leaves two drafts. ' +
247
247
  TIMEOUT_SENTENCE,
248
248
  // Self-only and additive, like outlook_reply with draftOnly: nothing is
249
249
  // overwritten or sent. Not idempotent: a repeat creates a second draft.
package/dist/version.js CHANGED
@@ -10,4 +10,4 @@
10
10
  * line below and nowhere else — not even in prose.
11
11
  */
12
12
  export const PACKAGE_NAME = 'office-outlook-mcp';
13
- export const VERSION = '0.10.1'; // x-release-please-version
13
+ export const VERSION = '0.11.0'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/office-outlook-mcp",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "mcpName": "io.github.chrischall/office-outlook-mcp",
5
5
  "description": "Outlook / Microsoft 365 MCP server for Claude — developed and maintained by AI (Claude Code)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/office-outlook-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.10.1",
9
+ "version": "0.11.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/office-outlook-mcp",
14
- "version": "0.10.1",
14
+ "version": "0.11.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },