@littlebearapps/outlook-assistant 3.7.4 → 3.8.1

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/.env.example CHANGED
@@ -28,3 +28,22 @@ USE_TEST_MODE=false
28
28
  # device-code: No auth server needed, works remotely/headless
29
29
  # browser: Traditional OAuth redirect via localhost:3333
30
30
  # OUTLOOK_AUTH_METHOD=device-code
31
+
32
+ # Optional: OAuth audience — controls which Microsoft Identity Platform
33
+ # endpoint is used. Must match the Azure app registration's "Supported
34
+ # account types" setting:
35
+ # common — personal AND work/school accounts (default; multi-tenant + personal apps)
36
+ # consumers — personal Microsoft accounts only
37
+ # organizations — work/school accounts only
38
+ # <tenant-guid> — single-tenant
39
+ # Example: OUTLOOK_AUTH_AUDIENCE=consumers (for personal-account-only apps)
40
+ # OUTLOOK_AUTH_AUDIENCE=common
41
+
42
+ # Optional: Default timezone for calendar events when not explicitly
43
+ # specified by the caller. Use any IANA timezone identifier.
44
+ # Default: Australia/Melbourne
45
+ # Examples:
46
+ # OUTLOOK_DEFAULT_TIMEZONE=Europe/London
47
+ # OUTLOOK_DEFAULT_TIMEZONE=America/New_York
48
+ # OUTLOOK_DEFAULT_TIMEZONE=Asia/Tokyo
49
+ # OUTLOOK_DEFAULT_TIMEZONE=Australia/Melbourne
package/README.md CHANGED
@@ -23,8 +23,8 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
23
23
 
24
24
  <div align="center">
25
25
  <br />
26
- <a href="docs/demo/outlook-assistant-demo.mp4">
27
- <img src="docs/demo/outlook-assistant-demo.gif" alt="Outlook Assistant Demo — searching emails, reading, and drafting a reply" width="720" style="border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.12);" />
26
+ <a href="https://github.com/littlebearapps/outlook-assistant/blob/main/docs/demo/outlook-assistant-demo.mp4">
27
+ <img src="https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/demo/outlook-assistant-demo.gif" alt="Outlook Assistant Demo — searching emails, reading, and drafting a reply" width="720" style="border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.12);" />
28
28
  </a>
29
29
  <br />
30
30
  <sub>Search inbox → read &amp; summarise → draft a reply — all from the conversation</sub>
@@ -63,7 +63,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
63
63
  | Module | Tools | What You Can Do |
64
64
  |--------|------:|-----------------|
65
65
  | **Email** | 8 | `search-emails` (list/search/delta/conversations), `read-email` (content + forensic headers), `send-email` (with dry-run + mail tips), `draft` (create/update/send/delete/reply/forward), `update-email` (read status, flags), `attachments`, `export`, `get-mail-tips` |
66
- | **Calendar** | 3 | `list-events`, `create-event`, `manage-event` (decline/cancel/delete) |
66
+ | **Calendar** | 3 | `list-events`, `create-event`, `manage-event` (update/decline/cancel/delete) |
67
67
  | **Contacts** | 2 | `manage-contact` (list/search/get/create/update/delete), `search-people` |
68
68
  | **Categories** | 3 | `manage-category` (CRUD), `apply-category`, `manage-focused-inbox` |
69
69
  | **Settings** | 1 | `mailbox-settings` (get/set auto-replies/set working hours) |
@@ -336,6 +336,15 @@ USE_TEST_MODE=false
336
336
 
337
337
  > **Note:** The server also accepts `MS_CLIENT_ID` and `MS_CLIENT_SECRET` for backwards compatibility.
338
338
 
339
+ **Optional overrides** (v3.8.0+) — see [`.env.example`](.env.example) for the full list with commented worked examples:
340
+
341
+ | Variable | Purpose | Default |
342
+ |----------|---------|---------|
343
+ | `OUTLOOK_AUTH_AUDIENCE` | OAuth audience: `common`, `consumers` (personal-only Azure apps), `organizations`, or single-tenant GUID. Fixes `AADSTS9002331` for personal-only app registrations. | `common` |
344
+ | `OUTLOOK_DEFAULT_TIMEZONE` | IANA timezone applied to calendar events when callers don't pass one (e.g. `Europe/London`, `America/New_York`). | `Australia/Melbourne` |
345
+ | `OUTLOOK_MAX_EMAILS_PER_SESSION` | Cap on `send-email` + `draft send` per MCP server lifetime. | unlimited |
346
+ | `OUTLOOK_ALLOWED_RECIPIENTS` | Comma-separated allowlist of domains/addresses for sends, drafts, and rule forwards. | unrestricted |
347
+
339
348
  ### MCP Client Configuration
340
349
 
341
350
  See [Quick Start — Configure Your MCP Client](#3-configure-your-mcp-client) above for Claude Desktop, Claude Code, Cursor, and Windsurf configs.
package/advanced/index.js CHANGED
@@ -606,7 +606,8 @@ async function handleFindMeetingRooms(args) {
606
606
  const advancedTools = [
607
607
  {
608
608
  name: 'access-shared-mailbox',
609
- description: 'Read emails from a shared mailbox you have access to',
609
+ description:
610
+ 'List emails from a shared mailbox the signed-in user has been granted access to (read-only). Returns paged messages from the named `sharedMailbox` (or alias `email`) and `folder` (default `inbox`) with id/subject/from/receivedDateTime/preview — same shape as `search-emails` list mode. Requires that the shared mailbox has been delegated to the signed-in user in Exchange (admin-configured). Use `outputVerbosity` to control field count and `count` (default 25, max 50) for page size. For full search/filter capability over a shared mailbox, prefer `search-emails` with a folder path scoped to the shared mailbox.',
610
611
  annotations: {
611
612
  title: 'Shared Mailbox',
612
613
  readOnlyHint: true,
@@ -645,7 +646,8 @@ const advancedTools = [
645
646
  },
646
647
  {
647
648
  name: 'find-meeting-rooms',
648
- description: 'Search for meeting rooms in your organisation',
649
+ description:
650
+ "Discover bookable meeting rooms in the user's organisation via the Graph rooms endpoint (read-only). Returns room resources with displayName, emailAddress, building, floor, capacity, and bookingType — suitable for piping into `create-event` as attendees. Filter by `query` (matches name/email), `building`, `floor`, or minimum `capacity`. Returns empty list on personal accounts (the rooms endpoint is M365-only). Use `outputVerbosity` to control field count.",
649
651
  annotations: {
650
652
  title: 'Meeting Rooms',
651
653
  readOnlyHint: true,
package/auth/tools.js CHANGED
@@ -386,7 +386,7 @@ const authTools = [
386
386
  {
387
387
  name: 'auth',
388
388
  description:
389
- 'Manage authentication with Microsoft Graph API. action=status (default) checks auth state and refreshes tokens if needed, action=authenticate starts OAuth flow (device-code by default — no auth server needed), action=device-code-complete finishes device code auth after user enters code, action=about shows server info.',
389
+ 'Manage authentication with the Microsoft Graph API. action=`status` (default) returns the current auth state and auto-refreshes the access token if it\'s expired but the refresh token is still valid (~90-day window) — call this first to check before other tools. action=`authenticate` starts the OAuth flow: with `method: "device-code"` (default, works headlessly) it returns a code + URL for the user to visit; with `method: "browser"` it opens the local auth server on :3333 (run `npm run auth-server` first). Pass `force: true` to re-authenticate over an existing valid session. action=`device-code-complete` finishes device-code auth after the user enters the code in their browser — call this once authentication shows as successful in the browser. action=`about` returns server version, configured audience, scope list, and other diagnostic info. Tokens persist to `~/.outlook-assistant-tokens.json` and survive server restarts.',
390
390
  annotations: {
391
391
  title: 'Authentication',
392
392
  readOnlyHint: false,
package/calendar/index.js CHANGED
@@ -6,12 +6,14 @@ const handleDeclineEvent = require('./decline');
6
6
  const handleCreateEvent = require('./create');
7
7
  const handleCancelEvent = require('./cancel');
8
8
  const handleDeleteEvent = require('./delete');
9
+ const handleUpdateEvent = require('./update');
9
10
 
10
11
  // Calendar tool definitions (consolidated: 5 → 3)
11
12
  const calendarTools = [
12
13
  {
13
14
  name: 'list-events',
14
- description: 'Lists upcoming events from your calendar',
15
+ description:
16
+ 'List upcoming calendar events for the signed-in user (read-only). Returns an array of events with id, subject, start/end, attendees, location, organiser, and webLink. Use `count` (default 10, max 50) to control page size; this tool does not filter — use the Outlook UI or specific date ranges via Graph for filtered queries. Times are returned in the configured timezone (default Australia/Melbourne; override with `OUTLOOK_DEFAULT_TIMEZONE`).',
15
17
  annotations: {
16
18
  title: 'List Calendar Events',
17
19
  readOnlyHint: true,
@@ -32,7 +34,8 @@ const calendarTools = [
32
34
  },
33
35
  {
34
36
  name: 'create-event',
35
- description: 'Creates a new calendar event',
37
+ description:
38
+ "Create a new calendar event on the signed-in user's default calendar. Returns the created event with its `id`, `webLink`, and (if attendees are present) an auto-generated online-meeting URL — attendees receive invitations on save. Times use the configured timezone (default Australia/Melbourne; override with `OUTLOOK_DEFAULT_TIMEZONE`); omit the `Z` suffix to send local time. Use `manage-event` action=`update` to modify an event after creation, or `manage-event` action=`cancel`/`delete` to remove it.",
36
39
  annotations: {
37
40
  title: 'Create Calendar Event',
38
41
  readOnlyHint: false,
@@ -74,7 +77,7 @@ const calendarTools = [
74
77
  {
75
78
  name: 'manage-event',
76
79
  description:
77
- 'Manage an existing calendar event. action=decline declines an invitation. action=cancel cancels an event you organised. action=delete permanently removes an event.',
80
+ "Manage an existing calendar event (destructive: covers update/decline/cancel/delete — use dryRun where supported to preview). action=`update` edits fields in place via PATCH (subject, start, end, attendees, body, location, isOnlineMeeting, sensitivity, showAs, importance, categories, reminderMinutesBeforeStart) — only fields you pass are changed; pass `dryRun: true` to preview the PATCH payload. action=`decline` declines an invitation (optional `comment`). action=`cancel` cancels an event you organised and notifies attendees. action=`delete` permanently removes the event. Returns the updated event on update; status confirmation otherwise. Note: there is no `accept` action — accept invitations in the Outlook UI (Graph's accept verb is unreliable across personal/M365).",
78
81
  annotations: {
79
82
  title: 'Manage Calendar Event',
80
83
  readOnlyHint: false,
@@ -86,7 +89,7 @@ const calendarTools = [
86
89
  properties: {
87
90
  action: {
88
91
  type: 'string',
89
- enum: ['decline', 'cancel', 'delete'],
92
+ enum: ['update', 'decline', 'cancel', 'delete'],
90
93
  description: 'Action to perform (required)',
91
94
  },
92
95
  eventId: {
@@ -102,6 +105,98 @@ const calendarTools = [
102
105
  type: 'string',
103
106
  description: 'Optional comment for declining or cancelling the event',
104
107
  },
108
+ subject: {
109
+ type: 'string',
110
+ description: 'New subject (action=update only)',
111
+ },
112
+ start: {
113
+ oneOf: [
114
+ { type: 'string' },
115
+ {
116
+ type: 'object',
117
+ properties: {
118
+ dateTime: { type: 'string' },
119
+ timeZone: { type: 'string' },
120
+ },
121
+ required: ['dateTime'],
122
+ additionalProperties: false,
123
+ },
124
+ ],
125
+ description:
126
+ 'New start time as ISO 8601 string or {dateTime, timeZone} object (action=update only)',
127
+ },
128
+ end: {
129
+ oneOf: [
130
+ { type: 'string' },
131
+ {
132
+ type: 'object',
133
+ properties: {
134
+ dateTime: { type: 'string' },
135
+ timeZone: { type: 'string' },
136
+ },
137
+ required: ['dateTime'],
138
+ additionalProperties: false,
139
+ },
140
+ ],
141
+ description:
142
+ 'New end time as ISO 8601 string or {dateTime, timeZone} object (action=update only)',
143
+ },
144
+ attendees: {
145
+ type: 'array',
146
+ items: { type: 'string' },
147
+ description:
148
+ 'Full replacement attendee list — pass complete desired list, or [] to clear (action=update only)',
149
+ },
150
+ body: {
151
+ type: 'string',
152
+ description: 'New body content (action=update only)',
153
+ },
154
+ location: {
155
+ type: 'string',
156
+ description: 'New location display name (action=update only)',
157
+ },
158
+ isOnlineMeeting: {
159
+ type: 'boolean',
160
+ description: 'Toggle online meeting flag (action=update only)',
161
+ },
162
+ sensitivity: {
163
+ type: 'string',
164
+ enum: ['normal', 'personal', 'private', 'confidential'],
165
+ description: 'Event sensitivity classification (action=update only)',
166
+ },
167
+ showAs: {
168
+ type: 'string',
169
+ enum: [
170
+ 'free',
171
+ 'tentative',
172
+ 'busy',
173
+ 'oof',
174
+ 'workingElsewhere',
175
+ 'unknown',
176
+ ],
177
+ description: 'Free/busy status shown to others (action=update only)',
178
+ },
179
+ importance: {
180
+ type: 'string',
181
+ enum: ['low', 'normal', 'high'],
182
+ description: 'Event importance flag (action=update only)',
183
+ },
184
+ categories: {
185
+ type: 'array',
186
+ items: { type: 'string' },
187
+ description:
188
+ 'Full replacement category list — pass [] to clear (action=update only)',
189
+ },
190
+ reminderMinutesBeforeStart: {
191
+ type: 'number',
192
+ description:
193
+ 'Minutes before start to fire the reminder (action=update only)',
194
+ },
195
+ dryRun: {
196
+ type: 'boolean',
197
+ description:
198
+ 'Preview the PATCH without applying it (action=update only). Returns the body that would be sent to Graph.',
199
+ },
105
200
  },
106
201
  additionalProperties: false,
107
202
  required: ['action'],
@@ -126,6 +221,8 @@ const calendarTools = [
126
221
  }
127
222
  args = normalised;
128
223
  switch (args.action) {
224
+ case 'update':
225
+ return handleUpdateEvent(args);
129
226
  case 'decline':
130
227
  return handleDeclineEvent(args);
131
228
  case 'cancel':
@@ -137,7 +234,7 @@ const calendarTools = [
137
234
  content: [
138
235
  {
139
236
  type: 'text',
140
- text: "Invalid action. Use 'decline', 'cancel', or 'delete'.",
237
+ text: "Invalid action. Use 'update', 'decline', 'cancel', or 'delete'.",
141
238
  },
142
239
  ],
143
240
  };
@@ -153,4 +250,5 @@ module.exports = {
153
250
  handleCreateEvent,
154
251
  handleCancelEvent,
155
252
  handleDeleteEvent,
253
+ handleUpdateEvent,
156
254
  };
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Update event functionality.
3
+ *
4
+ * Wraps the Microsoft Graph PATCH /me/events/{id} endpoint. Sends only
5
+ * the fields the caller provides — anything left out is preserved
6
+ * server-side. Useful for re-scheduling, adding/removing attendees,
7
+ * editing the body, or tweaking metadata without rebuilding the event
8
+ * from scratch (which loses RSVP state).
9
+ *
10
+ * Supports the following Graph event properties:
11
+ * - subject
12
+ * - start (ISO string or {dateTime, timeZone} object)
13
+ * - end (same shape as start)
14
+ * - attendees (full replacement list of emails)
15
+ * - body (sent as HTML)
16
+ * - location (displayName)
17
+ * - isOnlineMeeting
18
+ * - sensitivity (normal | personal | private | confidential)
19
+ * - showAs (free | tentative | busy | oof | workingElsewhere | unknown)
20
+ * - importance (low | normal | high)
21
+ * - categories (full replacement array of category names)
22
+ * - reminderMinutesBeforeStart
23
+ *
24
+ * `dryRun: true` returns a preview of the PATCH payload without calling
25
+ * Graph — useful for confirming behaviour before mutating real data.
26
+ */
27
+ const { callGraphAPI } = require('../utils/graph-api');
28
+ const { ensureAuthenticated } = require('../auth');
29
+ const { DEFAULT_TIMEZONE } = require('../config');
30
+
31
+ const SENSITIVITY_VALUES = new Set([
32
+ 'normal',
33
+ 'personal',
34
+ 'private',
35
+ 'confidential',
36
+ ]);
37
+ const SHOW_AS_VALUES = new Set([
38
+ 'free',
39
+ 'tentative',
40
+ 'busy',
41
+ 'oof',
42
+ 'workingElsewhere',
43
+ 'unknown',
44
+ ]);
45
+ const IMPORTANCE_VALUES = new Set(['low', 'normal', 'high']);
46
+
47
+ /**
48
+ * Update event handler
49
+ * @param {object} args - Tool arguments
50
+ * @returns {object} - MCP response
51
+ */
52
+ async function handleUpdateEvent(args) {
53
+ const {
54
+ eventId,
55
+ subject,
56
+ start,
57
+ end,
58
+ attendees,
59
+ body,
60
+ location,
61
+ isOnlineMeeting,
62
+ sensitivity,
63
+ showAs,
64
+ importance,
65
+ categories,
66
+ reminderMinutesBeforeStart,
67
+ dryRun = false,
68
+ } = args;
69
+
70
+ if (!eventId) {
71
+ return {
72
+ content: [
73
+ {
74
+ type: 'text',
75
+ text: 'Event ID is required to update an event.',
76
+ },
77
+ ],
78
+ };
79
+ }
80
+
81
+ // Build the patch body from only the fields the caller actually provided.
82
+ // Graph treats absent properties as "no change", so we never overwrite
83
+ // something the user didn't intend to touch.
84
+ const patch = {};
85
+
86
+ if (subject !== undefined) patch.subject = subject;
87
+
88
+ if (start !== undefined) {
89
+ patch.start = {
90
+ dateTime: start.dateTime || start,
91
+ timeZone: start.timeZone || DEFAULT_TIMEZONE,
92
+ };
93
+ }
94
+
95
+ if (end !== undefined) {
96
+ patch.end = {
97
+ dateTime: end.dateTime || end,
98
+ timeZone: end.timeZone || DEFAULT_TIMEZONE,
99
+ };
100
+ }
101
+
102
+ if (attendees !== undefined) {
103
+ // Replaces the full attendee list — Graph PATCH on this property is
104
+ // not additive. Caller must pass the desired complete list.
105
+ patch.attendees = (attendees || []).map((email) => ({
106
+ emailAddress: { address: email },
107
+ type: 'required',
108
+ }));
109
+ }
110
+
111
+ if (body !== undefined) {
112
+ patch.body = { contentType: 'HTML', content: body };
113
+ }
114
+
115
+ if (location !== undefined) {
116
+ patch.location = { displayName: location };
117
+ }
118
+
119
+ if (isOnlineMeeting !== undefined) {
120
+ patch.isOnlineMeeting = Boolean(isOnlineMeeting);
121
+ }
122
+
123
+ if (sensitivity !== undefined) {
124
+ if (!SENSITIVITY_VALUES.has(sensitivity)) {
125
+ return {
126
+ content: [
127
+ {
128
+ type: 'text',
129
+ text: `Invalid sensitivity: '${sensitivity}'. Must be one of: ${[...SENSITIVITY_VALUES].join(', ')}.`,
130
+ },
131
+ ],
132
+ };
133
+ }
134
+ patch.sensitivity = sensitivity;
135
+ }
136
+
137
+ if (showAs !== undefined) {
138
+ if (!SHOW_AS_VALUES.has(showAs)) {
139
+ return {
140
+ content: [
141
+ {
142
+ type: 'text',
143
+ text: `Invalid showAs: '${showAs}'. Must be one of: ${[...SHOW_AS_VALUES].join(', ')}.`,
144
+ },
145
+ ],
146
+ };
147
+ }
148
+ patch.showAs = showAs;
149
+ }
150
+
151
+ if (importance !== undefined) {
152
+ if (!IMPORTANCE_VALUES.has(importance)) {
153
+ return {
154
+ content: [
155
+ {
156
+ type: 'text',
157
+ text: `Invalid importance: '${importance}'. Must be one of: ${[...IMPORTANCE_VALUES].join(', ')}.`,
158
+ },
159
+ ],
160
+ };
161
+ }
162
+ patch.importance = importance;
163
+ }
164
+
165
+ if (categories !== undefined) {
166
+ // Full replacement, like attendees. Pass [] to clear all categories.
167
+ patch.categories = Array.isArray(categories) ? categories : [];
168
+ }
169
+
170
+ if (reminderMinutesBeforeStart !== undefined) {
171
+ const reminder = Number(reminderMinutesBeforeStart);
172
+ if (!Number.isFinite(reminder) || reminder < 0) {
173
+ return {
174
+ content: [
175
+ {
176
+ type: 'text',
177
+ text: `Invalid reminderMinutesBeforeStart: '${reminderMinutesBeforeStart}'. Must be a non-negative number.`,
178
+ },
179
+ ],
180
+ };
181
+ }
182
+ patch.reminderMinutesBeforeStart = reminder;
183
+ }
184
+
185
+ if (Object.keys(patch).length === 0) {
186
+ return {
187
+ content: [
188
+ {
189
+ type: 'text',
190
+ text: 'No fields to update — provide at least one updatable field (subject, start, end, attendees, body, location, isOnlineMeeting, sensitivity, showAs, importance, categories, reminderMinutesBeforeStart).',
191
+ },
192
+ ],
193
+ };
194
+ }
195
+
196
+ // dryRun: don't touch Graph; just show the caller what would be sent.
197
+ if (dryRun) {
198
+ return {
199
+ content: [
200
+ {
201
+ type: 'text',
202
+ text: [
203
+ `**Dry run** — would PATCH \`me/events/${eventId}\` with:`,
204
+ '',
205
+ '```json',
206
+ JSON.stringify(patch, null, 2),
207
+ '```',
208
+ '',
209
+ `Fields that would change: ${Object.keys(patch).join(', ')}`,
210
+ ].join('\n'),
211
+ },
212
+ ],
213
+ _meta: {
214
+ eventId,
215
+ dryRun: true,
216
+ patch,
217
+ fieldsChanged: Object.keys(patch),
218
+ },
219
+ };
220
+ }
221
+
222
+ try {
223
+ const accessToken = await ensureAuthenticated();
224
+ const endpoint = `me/events/${eventId}`;
225
+
226
+ const response = await callGraphAPI(accessToken, 'PATCH', endpoint, patch);
227
+
228
+ const output = [
229
+ `Event '${response.subject || eventId}' updated successfully.`,
230
+ ];
231
+ if (response.id) {
232
+ output.push(`**ID**: \`${response.id}\``);
233
+ }
234
+ if (response.start) {
235
+ output.push(
236
+ `**Start**: ${response.start.dateTime} (${response.start.timeZone})`
237
+ );
238
+ }
239
+ if (response.end) {
240
+ output.push(
241
+ `**End**: ${response.end.dateTime} (${response.end.timeZone})`
242
+ );
243
+ }
244
+ if (response.webLink) {
245
+ output.push(`**Link**: ${response.webLink}`);
246
+ }
247
+ output.push(`\nFields changed: ${Object.keys(patch).join(', ')}`);
248
+
249
+ return {
250
+ content: [
251
+ {
252
+ type: 'text',
253
+ text: output.join('\n'),
254
+ },
255
+ ],
256
+ _meta: {
257
+ eventId: response.id,
258
+ subject: response.subject,
259
+ start: response.start,
260
+ end: response.end,
261
+ fieldsChanged: Object.keys(patch),
262
+ },
263
+ };
264
+ } catch (error) {
265
+ if (error.message === 'Authentication required') {
266
+ return {
267
+ content: [
268
+ {
269
+ type: 'text',
270
+ text: "Authentication required. Please use the 'authenticate' tool first.",
271
+ },
272
+ ],
273
+ };
274
+ }
275
+
276
+ return {
277
+ content: [
278
+ {
279
+ type: 'text',
280
+ text: `Error updating event: ${error.message}`,
281
+ },
282
+ ],
283
+ };
284
+ }
285
+ }
286
+
287
+ module.exports = handleUpdateEvent;
@@ -826,7 +826,7 @@ const categoriesTools = [
826
826
  {
827
827
  name: 'manage-category',
828
828
  description:
829
- 'Manage master categories. action=list (default) lists categories. action=create creates a category. action=update changes name/color. action=delete removes a category.',
829
+ "Manage the user's master category list (the colour-coded labels available across mail/calendar/contacts). action=`list` (default) returns categories with id/displayName/color. action=`create` adds a new category — `displayName` required, `color` optional (preset0-preset24, e.g. preset0=Red, preset7=Blue). action=`update` (alias `set` — deprecated) changes name/colour by `id`. action=`delete` removes a category — this does NOT untag messages already labelled with it; existing messages retain the orphaned label until manually cleaned. Use `apply-category` to tag/untag specific messages.",
830
830
  annotations: {
831
831
  title: 'Master Categories',
832
832
  readOnlyHint: false,
@@ -899,7 +899,8 @@ const categoriesTools = [
899
899
  },
900
900
  {
901
901
  name: 'apply-category',
902
- description: 'Apply, add, or remove categories on email message(s)',
902
+ description:
903
+ "Tag or untag email messages with master categories (those created via `manage-category`). action=`set` (default) replaces the message's category set with the supplied `categories` array. action=`add` appends categories to whatever's already on the message. action=`remove` removes only the named categories, leaving the rest. Accepts either `messageId` (single) or `messageIds` (batch via Graph `$batch`). `categories` are matched by display name — names must already exist in the master list (create via `manage-category` first). Returns per-message confirmation.",
903
904
  annotations: {
904
905
  title: 'Apply Categories',
905
906
  readOnlyHint: false,
@@ -938,7 +939,7 @@ const categoriesTools = [
938
939
  {
939
940
  name: 'manage-focused-inbox',
940
941
  description:
941
- 'Manage Focused Inbox overrides. action=list (default) shows overrides. action=set creates/updates an override. action=delete removes an override.',
942
+ 'Manage Focused Inbox sender overrides — explicit rules that force messages from a given sender into Focused or Other regardless of the ML classifier. action=`list` (default) returns existing overrides with id/sender/classifyAs. action=`set` creates or updates an override for `emailAddress` (optional `name`), routing future mail to `focused` (default) or `other`. action=`delete` removes the override for `emailAddress`. Note: this only works on accounts that have Focused Inbox enabled — personal Outlook.com accounts without it return an empty list.',
942
943
  annotations: {
943
944
  title: 'Focused Inbox',
944
945
  readOnlyHint: false,
package/config.js CHANGED
@@ -23,6 +23,48 @@ if (!homeDir) {
23
23
  );
24
24
  }
25
25
 
26
+ /**
27
+ * Resolve the OAuth audience segment used in Microsoft Graph endpoints.
28
+ *
29
+ * Microsoft's identity platform v2.0 routes by audience:
30
+ * - `common` — personal AND work/school accounts (multi-tenant + personal)
31
+ * - `consumers` — personal Microsoft accounts only
32
+ * - `organizations` — work/school accounts only
33
+ * - `<tenant-guid>` — single-tenant
34
+ *
35
+ * The right value depends on the Azure app registration's "Supported account
36
+ * types" setting. An app registered as "Personal Microsoft accounts only" is
37
+ * rejected by `/common/` with `AADSTS9002331` and must use `/consumers/`;
38
+ * a single-tenant app must use its tenant GUID; etc.
39
+ *
40
+ * Defaulting to `common` preserves existing behaviour. Set
41
+ * `OUTLOOK_AUTH_AUDIENCE` to override.
42
+ */
43
+ const AUTH_AUDIENCE = process.env.OUTLOOK_AUTH_AUDIENCE || 'common';
44
+
45
+ // Surface obvious misconfigurations at startup rather than failing later with a
46
+ // cryptic AADSTS error from Microsoft. Warn rather than throw so we never break
47
+ // an existing deployment on upgrade — Graph itself remains the source of truth
48
+ // for what audiences it accepts.
49
+ const VALID_AUDIENCE_LITERALS = new Set([
50
+ 'common',
51
+ 'consumers',
52
+ 'organizations',
53
+ ]);
54
+ const TENANT_GUID_RE =
55
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
56
+ if (
57
+ !VALID_AUDIENCE_LITERALS.has(AUTH_AUDIENCE) &&
58
+ !TENANT_GUID_RE.test(AUTH_AUDIENCE)
59
+ ) {
60
+ // eslint-disable-next-line no-console
61
+ console.warn(
62
+ `[outlook-assistant] OUTLOOK_AUTH_AUDIENCE="${AUTH_AUDIENCE}" is not a recognised value. ` +
63
+ `Expected one of: common, consumers, organizations, or a tenant GUID. ` +
64
+ `Proceeding anyway — Microsoft's identity platform will reject it at runtime if invalid.`
65
+ );
66
+ }
67
+
26
68
  module.exports = {
27
69
  // Server information
28
70
  SERVER_NAME: 'outlook-assistant',
@@ -54,9 +96,10 @@ module.exports = {
54
96
  ],
55
97
  tokenStorePath: path.join(homeDir, '.outlook-assistant-tokens.json'),
56
98
  authServerUrl: 'http://localhost:3333',
57
- deviceCodeEndpoint:
58
- 'https://login.microsoftonline.com/common/oauth2/v2.0/devicecode',
59
- tokenEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
99
+ audience: AUTH_AUDIENCE,
100
+ deviceCodeEndpoint: `https://login.microsoftonline.com/${AUTH_AUDIENCE}/oauth2/v2.0/devicecode`,
101
+ tokenEndpoint: `https://login.microsoftonline.com/${AUTH_AUDIENCE}/oauth2/v2.0/token`,
102
+ authorizeEndpoint: `https://login.microsoftonline.com/${AUTH_AUDIENCE}/oauth2/v2.0/authorize`,
60
103
  defaultAuthMethod: process.env.OUTLOOK_AUTH_METHOD || 'device-code',
61
104
  },
62
105
 
@@ -97,6 +140,9 @@ module.exports = {
97
140
  // Immutable IDs (opt-in: IDs persist through folder moves)
98
141
  USE_IMMUTABLE_IDS: process.env.OUTLOOK_IMMUTABLE_IDS === 'true',
99
142
 
100
- // Timezone
101
- DEFAULT_TIMEZONE: 'Australia/Melbourne', // Updated for Nathan's timezone
143
+ // Timezone — IANA zone (e.g. "Australia/Melbourne", "Europe/London",
144
+ // "America/New_York"). Override per-deployment via OUTLOOK_DEFAULT_TIMEZONE.
145
+ // Default preserves the historic value for backwards compatibility.
146
+ DEFAULT_TIMEZONE:
147
+ process.env.OUTLOOK_DEFAULT_TIMEZONE || 'Australia/Melbourne',
102
148
  };
package/contacts/index.js CHANGED
@@ -679,11 +679,11 @@ const contactsTools = [
679
679
  {
680
680
  name: 'manage-contact',
681
681
  description:
682
- 'Manage personal contacts. action=list (default) lists contacts. action=search searches by name/email. action=get retrieves full details. action=create adds a contact. action=update modifies a contact. action=delete removes a contact.',
682
+ "Full CRUD over the signed-in user's personal Outlook contacts (destructive: covers `delete` action). action=`list` (default) returns contacts with pagination via `skip`/`count` (default 50). action=`search` returns contacts matching `query` against name/email (default 25). action=`get` returns full contact detail by `id`. action=`create` adds a new contact and returns its `id`. action=`update` patches the given fields by `id` (only fields passed are changed). action=`delete` permanently removes the contact by `id`. Use `outputVerbosity` (minimal/standard/full) on list/search to control field count. Prefer `search-people` for cross-source relevance ranking (contacts + directory + recent comms) — this tool only searches your personal contact store.",
683
683
  annotations: {
684
684
  title: 'Contacts',
685
685
  readOnlyHint: false,
686
- destructiveHint: false,
686
+ destructiveHint: true,
687
687
  openWorldHint: false,
688
688
  },
689
689
  inputSchema: {
@@ -801,7 +801,7 @@ const contactsTools = [
801
801
  {
802
802
  name: 'search-people',
803
803
  description:
804
- 'Search for people by relevance (includes contacts, directory, and recent communications). Uses a different API from manage-contact search.',
804
+ 'Relevance-ranked search across personal contacts, organisation directory, and recent communications via the Microsoft Graph People API (read-only). Returns people objects with `displayName`, `emailAddresses`, `companyName`, `jobTitle`, and relevance metadata — ideal for "who is X?" or "who do I email about Y?" lookups. Use `manage-contact` action=`search` instead when you specifically need entries from your personal contact store only.',
805
805
  annotations: {
806
806
  title: 'People Search',
807
807
  readOnlyHint: true,
package/email/index.js CHANGED
@@ -33,7 +33,7 @@ const emailTools = [
33
33
  {
34
34
  name: 'search-emails',
35
35
  description:
36
- 'Search and list emails. With no query, lists recent emails (like list-emails). Supports search queries, KQL, delta sync, message-id lookup, and conversation listing.',
36
+ 'Search, list, delta-sync, or thread-group emails — six modes selected by parameters (read-only). With no params: lists recent emails in `folder` (default `inbox`). With `query`/`from`/`to`/`subject`/date filters: full search (combines via OData filter). With `kqlQuery`: raw Keyword Query Language for advanced server-side search. With `deltaMode: true`: returns current state plus a `deltaToken`; pass the token back on the next call for incremental changes only — ideal for inbox monitoring. With `groupByConversation: true`: returns conversation threads. With `conversationId`: returns all messages in a single thread. With `internetMessageId`: looks up a message by its RFC Message-ID header. Personal Outlook.com accounts have limited `$search` support — this tool falls back through OData filters / boolean filters / recent listing automatically, but structured filters (`from`/`subject`/`receivedAfter`/`hasAttachments`/`unreadOnly`) return cleaner results. Returns paged messages with id/subject/from/receivedDateTime/preview by default; use `outputVerbosity` to expand.',
37
37
  annotations: {
38
38
  title: 'Search Emails',
39
39
  readOnlyHint: true,
@@ -179,7 +179,7 @@ const emailTools = [
179
179
  {
180
180
  name: 'read-email',
181
181
  description:
182
- 'Read email content. Set headersMode=true for forensic headers (DKIM, SPF, Received, Message-ID).',
182
+ 'Read a single email by id (read-only). Default: returns the full message body (HTML stripped to text by default), subject, from/to/cc, receivedDateTime, conversationId, attachments metadata, and webLink as Markdown. With `headersMode: true`: returns RFC-822 forensic headers instead (DKIM, SPF, DMARC, Received chain, Message-ID, Authentication-Results) — pair with `importantOnly: true` for the security-relevant subset, `groupByType: true` for category-bucketed view, or `raw: true` for JSON instead of Markdown. With `includeHeaders: true` (non-headers-mode): adds basic headers alongside body. Use `outputVerbosity` (minimal/standard/full) to control field count.',
183
183
  annotations: {
184
184
  title: 'Read Email',
185
185
  readOnlyHint: true,
@@ -237,7 +237,7 @@ const emailTools = [
237
237
  {
238
238
  name: 'send-email',
239
239
  description:
240
- 'Compose and send an email. Use dryRun=true to preview without sending. Subject to rate limits and recipient allowlist when configured.',
240
+ 'Compose and send an email immediately (destructive: sends external comms). Returns a confirmation with the saved-message id. Safety controls: `dryRun: true` returns the composed message for review without sending; `checkRecipients: true` runs `get-mail-tips` first to flag out-of-office / mailbox-full / delivery-restricted / external recipients; combine both for a full pre-send review. Subject to session rate limits (`OUTLOOK_MAX_EMAILS_PER_SESSION` env) and recipient allowlist (`OUTLOOK_ALLOWED_RECIPIENTS` env) when configured — calls outside the allowlist fail before any Graph request. For multi-step compose/review workflows prefer `draft` (action=`create` → `update` → `send`) since drafts can be inspected in Outlook before sending. Comma-separated recipient strings or arrays both accepted.',
241
241
  annotations: {
242
242
  title: 'Send Email',
243
243
  readOnlyHint: false,
@@ -296,7 +296,7 @@ const emailTools = [
296
296
  {
297
297
  name: 'draft',
298
298
  description:
299
- 'Manage email drafts. action=create saves a new draft. action=update edits an existing draft. action=send sends a draft. action=delete removes a draft. action=reply/reply-all/forward creates a reply or forward draft from an existing message.',
299
+ 'Full draft lifecycle for review-before-send workflows (destructive: covers `send` and `delete`). action=`create` saves a new draft in the Drafts folder and returns its id (use `dryRun: true` to preview without saving; `checkRecipients: true` runs mail-tips first). action=`update` patches an existing draft by `id` (only fields passed are changed). action=`send` dispatches an existing draft — shares the rate limit with `send-email`. action=`delete` removes a draft permanently. action=`reply`/`reply-all` creates a reply draft from a message `id` (use `comment` to prepend text — mutually exclusive with `body`). action=`forward` creates a forward draft (requires `id` and `to`). Recipient allowlist applies to create/update/forward. Returns the draft object on create/update/reply/forward; status confirmation on send/delete.',
300
300
  annotations: {
301
301
  title: 'Draft Operations',
302
302
  readOnlyHint: false,
@@ -375,7 +375,7 @@ const emailTools = [
375
375
  {
376
376
  name: 'update-email',
377
377
  description:
378
- 'Update email state. action=mark-read/mark-unread changes read status. action=flag sets follow-up flag. action=unflag clears flag. action=complete marks flag as done.',
378
+ 'Update message state without modifying content (idempotent — safe to retry). action=`mark-read`/`mark-unread` toggles the `isRead` flag on a single message by `id`. action=`flag` sets a follow-up flag with optional `dueDateTime`/`startDateTime` (ISO 8601). action=`unflag` clears the flag. action=`complete` marks the flag as done. Flag/unflag/complete accept either `id` (single) or `ids` (batch array) — batch operations use Graph `$batch` for efficiency. Returns status confirmation per message.',
379
379
  annotations: {
380
380
  title: 'Update Email',
381
381
  readOnlyHint: false,
@@ -455,7 +455,7 @@ const emailTools = [
455
455
  {
456
456
  name: 'attachments',
457
457
  description:
458
- 'Manage email attachments. action=list shows attachments for a message. action=view shows content/metadata. action=download saves to disk.',
458
+ 'Inspect or retrieve email attachments. action=`list` (default) returns metadata for all attachments on `messageId` (id, name, contentType, size, isInline) — read-only. action=`view` returns inline content for text/JSON/XML attachments via `attachmentId`; binary types require download. action=`download` saves the attachment to disk at `outputDir` (default system tmpdir, auto-created) and returns the saved file path. `messageId` is required for all actions; `attachmentId` is required for view/download. Use `outputVerbosity` to control list field count.',
459
459
  annotations: {
460
460
  title: 'Attachments',
461
461
  readOnlyHint: false,
@@ -516,7 +516,7 @@ const emailTools = [
516
516
  {
517
517
  name: 'export',
518
518
  description:
519
- 'Export emails. target=message exports one email. target=messages batch-exports. target=conversation exports a thread. target=mime gets raw MIME/EML content.',
519
+ 'Export emails to file formats for archival, forensics, or programmatic processing. target=`message` (default) exports a single email by `id` to `savePath` — accepts `mime`/`eml`/`markdown`/`json`/`csv`. target=`messages` batch-exports either an explicit `emailIds` array or messages matching `searchQuery` (or `query` shortcut) into `outputDir` — accepts `markdown`/`json`/`csv`. target=`conversation` exports a full thread by `conversationId` into `outputDir` (chronological by default; pass `order: "reverse"` for newest-first) — accepts `eml`/`mbox`/`markdown`/`json`/`html`/`csv`. target=`mime` returns raw RFC-822 MIME bytes for `id` (use `headersOnly` for just headers, `base64` for encoded transport, `maxSize` to cap at default 1MB). `includeAttachments` defaults to true for single-message exports and false for batch. Format support varies by target — see the format param enum.',
520
520
  annotations: {
521
521
  title: 'Export Emails',
522
522
  readOnlyHint: false,
@@ -634,7 +634,7 @@ const emailTools = [
634
634
  {
635
635
  name: 'get-mail-tips',
636
636
  description:
637
- 'Check recipients before sending: out-of-office status, mailbox full, external recipients, delivery restrictions, moderation, group member counts, and max message size. No competitor offers this.',
637
+ 'Pre-send recipient validation via Graph `POST /me/getMailTips` (read-only; uses the existing `Mail.Read` scope — no extra permissions). Returns per-recipient tips covering automatic replies (out-of-office), mailbox full status, custom admin mail tips, delivery restrictions, moderation requirements, external-vs-internal scope, max message size, and group member counts (total + external). Use ahead of `send-email` or `draft` action=`create` to catch issues like OOO replies or external-recipient warnings before the message goes out; `send-email`/`draft` accept `checkRecipients: true` to invoke this automatically. Accepts either a comma-separated string or an array of addresses; `tipTypes` filters which tips are requested (defaults to all).',
638
638
  annotations: {
639
639
  title: 'Mail Tips',
640
640
  readOnlyHint: true,
package/folder/index.js CHANGED
@@ -12,7 +12,7 @@ const folderTools = [
12
12
  {
13
13
  name: 'folders',
14
14
  description:
15
- 'Manage mail folders. action=list (default) lists folders. action=create creates a folder. action=move moves emails between folders. action=stats gets folder counts for pagination planning. action=delete removes a folder.',
15
+ 'Manage mail folders (tool-level destructiveHint=true because `delete` permanently removes a folder; `list` and `stats` are read-only sub-actions despite the annotation). action=`list` (default) returns the folder tree with id/displayName/parentFolderId (toggle `includeItemCounts` for unread/total, `includeChildren` for hierarchy). action=`create` makes a new folder under the inbox (or under `folder`/`folderId`/`folderName`) and returns its id. action=`move` relocates emails (`emailIds` array) into `targetFolder`. action=`stats` returns counts (totalItemCount/unreadItemCount) suitable for pagination planning — pair with `outputVerbosity` to limit noise. action=`delete` permanently removes a folder and its contents — there is no recycle-bin recovery.',
16
16
  annotations: {
17
17
  title: 'Mail Folders',
18
18
  readOnlyHint: false,
package/llms.txt CHANGED
@@ -76,9 +76,9 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
76
76
  - [Connect Outlook to Claude](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/how-to/getting-started/connect-outlook-to-claude.md): Step-by-step setup guide for Claude Desktop / Claude Code
77
77
  - [Verify Your Connection](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/how-to/getting-started/verify-your-connection.md): Test and troubleshoot the connection after installation
78
78
  - [Azure Setup](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/guides/azure-setup.md): Azure app registration and API permissions walkthrough
79
- - [FAQ](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/faq/index.md): Frequently asked questions — install, accounts, permissions, tokens, send safety, updates, uninstall (also at <https://littlebearapps.com/help/outlook-assistant/faq/>)
79
+ - [FAQ](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/faq/faq.md): Frequently asked questions — install, accounts, permissions, tokens, send safety, updates, uninstall (also at <https://littlebearapps.com/help/outlook-assistant/faq/>)
80
80
  - [CLAUDE.md](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CLAUDE.md): Quick reference for development
81
81
  - [CONTRIBUTING](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CONTRIBUTING.md): Contribution guidelines
82
- - [CHANGELOG](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CHANGELOG.md): Version history (current: v3.7.4 — patch release closing two regressions surfaced by an independent v3.7.3 E2E re-verification: F-24 chokepoint now catches JSON-stringified arrays from MCP transport (#168) and search-emails kqlQuery no longer silently drops on Step 0 fall-through (#169))
83
- - [ROADMAP](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/ROADMAP.md): Active milestones (v3.7.5 polish, v3.8.0 task integration & auth, v3.9.0 new Graph APIs)
82
+ - [CHANGELOG](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CHANGELOG.md): Version history (current: v3.8.0 — `manage-event` gains an `update` action covering 12 fields with `dryRun` (#173, closes #124); new `OUTLOOK_AUTH_AUDIENCE` env var fixes AADSTS9002331 for personal-only Azure apps (#174); new `OUTLOOK_DEFAULT_TIMEZONE` env var overrides the hardcoded Australia/Melbourne default (#175))
83
+ - [ROADMAP](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/ROADMAP.md): Active milestones (v3.8.x polish, v3.9.0 new Graph APIs)
84
84
  - [SECURITY](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/SECURITY.md): Security policy, token handling, and MCP safety controls
@@ -39,7 +39,8 @@ const { AUTH_CONFIG: centralAuth } = require('./config');
39
39
  // Log to console
40
40
  console.log('Starting Outlook Authentication Server');
41
41
 
42
- // Authentication configuration — scopes and tokenStorePath from config.js (single source of truth)
42
+ // Authentication configuration — scopes, tokenStorePath, and audience-driven
43
+ // endpoints sourced from config.js (single source of truth).
43
44
  const AUTH_CONFIG = {
44
45
  clientId: process.env.OUTLOOK_CLIENT_ID || process.env.MS_CLIENT_ID || '',
45
46
  clientSecret:
@@ -47,6 +48,8 @@ const AUTH_CONFIG = {
47
48
  redirectUri: centralAuth.redirectUri,
48
49
  scopes: centralAuth.scopes,
49
50
  tokenStorePath: centralAuth.tokenStorePath,
51
+ authorizeEndpoint: centralAuth.authorizeEndpoint,
52
+ tokenEndpoint: centralAuth.tokenEndpoint,
50
53
  };
51
54
 
52
55
  // Create HTTP server
@@ -247,7 +250,9 @@ const server = http.createServer((req, res) => {
247
250
  state,
248
251
  };
249
252
 
250
- const authUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${querystring.stringify(authParams)}`;
253
+ // Use the audience from config (defaults to "common"; configurable via
254
+ // OUTLOOK_AUTH_AUDIENCE for personal-only / single-tenant Azure apps).
255
+ const authUrl = `${AUTH_CONFIG.authorizeEndpoint}?${querystring.stringify(authParams)}`;
251
256
  console.log(`Redirecting to: ${authUrl}`);
252
257
 
253
258
  // Redirect to Microsoft's login page
@@ -296,9 +301,13 @@ function exchangeCodeForTokens(code) {
296
301
  scope: AUTH_CONFIG.scopes.join(' '),
297
302
  });
298
303
 
304
+ // Same audience-driven path as the authorize URL above.
305
+ const tokenUrl = new URL(AUTH_CONFIG.tokenEndpoint);
299
306
  const options = {
300
- hostname: 'login.microsoftonline.com',
301
- path: '/common/oauth2/v2.0/token',
307
+ hostname: tokenUrl.hostname,
308
+ // Preserve any query string on the endpoint (none today, but future-safe
309
+ // if Microsoft ever adds hint params to the token URL).
310
+ path: tokenUrl.pathname + tokenUrl.search,
302
311
  method: 'POST',
303
312
  headers: {
304
313
  'Content-Type': 'application/x-www-form-urlencoded',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@littlebearapps/outlook-assistant",
3
- "version": "3.7.4",
3
+ "version": "3.8.1",
4
4
  "mcpName": "io.github.littlebearapps/outlook-assistant",
5
5
  "description": "Outlook Assistant — MCP server with 22 tools for email, calendar, contacts, and settings via Microsoft Graph API",
6
6
  "main": "index.js",
@@ -74,7 +74,7 @@
74
74
  "llms-install.md"
75
75
  ],
76
76
  "dependencies": {
77
- "@modelcontextprotocol/sdk": "^1.27.1",
77
+ "@modelcontextprotocol/sdk": "^1.29.0",
78
78
  "dotenv": "^17.3.1"
79
79
  },
80
80
  "devDependencies": {
package/rules/index.js CHANGED
@@ -180,7 +180,7 @@ const rulesTools = [
180
180
  {
181
181
  name: 'manage-rules',
182
182
  description:
183
- 'Manage inbox rules. action=list (default) lists rules. action=create creates a new rule with conditions, actions, and optional exceptions. action=update modifies an existing rule. action=reorder changes rule execution priority. action=delete removes a rule.',
183
+ 'Server-side inbox rule CRUD (destructive: covers `delete`; supports `dryRun` on create/update for preview). Rules run on the Exchange server regardless of which client is open. action=`list` (default) returns rules with id/name/sequence — pass `includeDetails: true` to expand conditions/actions/exceptions. action=`create` builds a new rule from condition params (12 supported: fromAddresses, containsSubject, bodyContains, hasAttachments, importance, sentTo, sensitivity, etc.), action params (9 supported: moveToFolder, forwardTo, redirectTo, assignCategories, markAsRead, delete, etc.), and optional `except*` exceptions. action=`update` patches the named fields by `ruleId`. action=`reorder` changes execution priority via `sequence` (lower = earlier). action=`delete` removes a rule. Recipient allowlist applies to forwardTo/redirectTo. `permanentDelete` action is intentionally omitted (too dangerous for AI use — use the Outlook UI). Subject to session rate limits (`OUTLOOK_MAX_MANAGE_RULES_PER_SESSION`).',
184
184
  annotations: {
185
185
  title: 'Inbox Rules',
186
186
  readOnlyHint: false,
package/settings/index.js CHANGED
@@ -615,7 +615,7 @@ const settingsTools = [
615
615
  {
616
616
  name: 'mailbox-settings',
617
617
  description:
618
- 'Manage mailbox settings. action=get (default) retrieves settings (language, timezone, working hours, auto-replies). action=set-auto-replies configures out-of-office. action=set-working-hours configures work schedule.',
618
+ 'Read or update mailbox-level settings (idempotent — safe to retry; sets are PATCH-style and merge with existing state). action=`get` (default) returns settings — use `section` to filter (`language`, `timeZone`, `workingHours`, `automaticRepliesSetting`, or `all`). action=`set-auto-replies` configures out-of-office: `enabled` true/false, optional `startDateTime`/`endDateTime` (ISO 8601) for scheduled mode, `internalReplyMessage` and (optionally) `externalReplyMessage`. action=`set-working-hours` updates the schedule: `startTime`/`endTime` (HH:MM) and `daysOfWeek` (array of `monday`..`sunday`). Returns the updated settings object on set actions.',
619
619
  annotations: {
620
620
  title: 'Mailbox Settings',
621
621
  readOnlyHint: false,