@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 +19 -0
- package/README.md +12 -3
- package/calendar/index.js +99 -3
- package/calendar/update.js +287 -0
- package/config.js +51 -5
- package/llms.txt +2 -2
- package/outlook-auth-server.js +13 -4
- package/package.json +2 -2
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 & 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
|
-
|
|
58
|
-
|
|
59
|
-
tokenEndpoint:
|
|
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
|
-
|
|
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.
|
|
83
|
-
- [ROADMAP](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/ROADMAP.md): Active milestones (v3.
|
|
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
|
package/outlook-auth-server.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
301
|
-
|
|
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.
|
|
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.
|
|
77
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
78
78
|
"dotenv": "^17.3.1"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|