@littlebearapps/outlook-assistant 3.7.4 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.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/calendar/index.js CHANGED
@@ -6,6 +6,7 @@ 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 = [
@@ -74,7 +75,7 @@ const calendarTools = [
74
75
  {
75
76
  name: 'manage-event',
76
77
  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.',
78
+ 'Manage an existing calendar event. action=update edits fields (subject, start, end, attendees, body, location, isOnlineMeeting, sensitivity, showAs, importance, categories, reminderMinutesBeforeStart) without rebuilding the event; pass dryRun=true to preview the PATCH payload. action=decline declines an invitation. action=cancel cancels an event you organised. action=delete permanently removes an event.',
78
79
  annotations: {
79
80
  title: 'Manage Calendar Event',
80
81
  readOnlyHint: false,
@@ -86,7 +87,7 @@ const calendarTools = [
86
87
  properties: {
87
88
  action: {
88
89
  type: 'string',
89
- enum: ['decline', 'cancel', 'delete'],
90
+ enum: ['update', 'decline', 'cancel', 'delete'],
90
91
  description: 'Action to perform (required)',
91
92
  },
92
93
  eventId: {
@@ -102,6 +103,98 @@ const calendarTools = [
102
103
  type: 'string',
103
104
  description: 'Optional comment for declining or cancelling the event',
104
105
  },
106
+ subject: {
107
+ type: 'string',
108
+ description: 'New subject (action=update only)',
109
+ },
110
+ start: {
111
+ oneOf: [
112
+ { type: 'string' },
113
+ {
114
+ type: 'object',
115
+ properties: {
116
+ dateTime: { type: 'string' },
117
+ timeZone: { type: 'string' },
118
+ },
119
+ required: ['dateTime'],
120
+ additionalProperties: false,
121
+ },
122
+ ],
123
+ description:
124
+ 'New start time as ISO 8601 string or {dateTime, timeZone} object (action=update only)',
125
+ },
126
+ end: {
127
+ oneOf: [
128
+ { type: 'string' },
129
+ {
130
+ type: 'object',
131
+ properties: {
132
+ dateTime: { type: 'string' },
133
+ timeZone: { type: 'string' },
134
+ },
135
+ required: ['dateTime'],
136
+ additionalProperties: false,
137
+ },
138
+ ],
139
+ description:
140
+ 'New end time as ISO 8601 string or {dateTime, timeZone} object (action=update only)',
141
+ },
142
+ attendees: {
143
+ type: 'array',
144
+ items: { type: 'string' },
145
+ description:
146
+ 'Full replacement attendee list — pass complete desired list, or [] to clear (action=update only)',
147
+ },
148
+ body: {
149
+ type: 'string',
150
+ description: 'New body content (action=update only)',
151
+ },
152
+ location: {
153
+ type: 'string',
154
+ description: 'New location display name (action=update only)',
155
+ },
156
+ isOnlineMeeting: {
157
+ type: 'boolean',
158
+ description: 'Toggle online meeting flag (action=update only)',
159
+ },
160
+ sensitivity: {
161
+ type: 'string',
162
+ enum: ['normal', 'personal', 'private', 'confidential'],
163
+ description: 'Event sensitivity classification (action=update only)',
164
+ },
165
+ showAs: {
166
+ type: 'string',
167
+ enum: [
168
+ 'free',
169
+ 'tentative',
170
+ 'busy',
171
+ 'oof',
172
+ 'workingElsewhere',
173
+ 'unknown',
174
+ ],
175
+ description: 'Free/busy status shown to others (action=update only)',
176
+ },
177
+ importance: {
178
+ type: 'string',
179
+ enum: ['low', 'normal', 'high'],
180
+ description: 'Event importance flag (action=update only)',
181
+ },
182
+ categories: {
183
+ type: 'array',
184
+ items: { type: 'string' },
185
+ description:
186
+ 'Full replacement category list — pass [] to clear (action=update only)',
187
+ },
188
+ reminderMinutesBeforeStart: {
189
+ type: 'number',
190
+ description:
191
+ 'Minutes before start to fire the reminder (action=update only)',
192
+ },
193
+ dryRun: {
194
+ type: 'boolean',
195
+ description:
196
+ 'Preview the PATCH without applying it (action=update only). Returns the body that would be sent to Graph.',
197
+ },
105
198
  },
106
199
  additionalProperties: false,
107
200
  required: ['action'],
@@ -126,6 +219,8 @@ const calendarTools = [
126
219
  }
127
220
  args = normalised;
128
221
  switch (args.action) {
222
+ case 'update':
223
+ return handleUpdateEvent(args);
129
224
  case 'decline':
130
225
  return handleDeclineEvent(args);
131
226
  case 'cancel':
@@ -137,7 +232,7 @@ const calendarTools = [
137
232
  content: [
138
233
  {
139
234
  type: 'text',
140
- text: "Invalid action. Use 'decline', 'cancel', or 'delete'.",
235
+ text: "Invalid action. Use 'update', 'decline', 'cancel', or 'delete'.",
141
236
  },
142
237
  ],
143
238
  };
@@ -153,4 +248,5 @@ module.exports = {
153
248
  handleCreateEvent,
154
249
  handleCancelEvent,
155
250
  handleDeleteEvent,
251
+ handleUpdateEvent,
156
252
  };
@@ -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;
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/llms.txt CHANGED
@@ -79,6 +79,6 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
79
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/>)
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.0",
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": {