@littlebearapps/outlook-assistant 3.5.1 → 3.6.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/README.md CHANGED
@@ -34,6 +34,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
34
34
 
35
35
  - 📨 **Search and read emails** — find messages by sender, subject, date, or keywords; read full threads with conversation grouping; batch flag, move, export, or categorise multiple emails at once
36
36
  - 🛡️ **Send emails with safety controls** — dry-run preview, pre-send mail tips (out-of-office, mailbox full, delivery restrictions), session rate limiting, and recipient allowlist to prevent mistakes
37
+ - ✏️ **Draft emails for review** — create, update, and send drafts; reply and forward as drafts; preview before saving with dry-run mode
37
38
  - 📅 **Manage your calendar** — view upcoming events, schedule meetings with attendees, decline or cancel invitations
38
39
  - 📦 **Export emails** — save to Markdown, EML, MBOX, JSON, or HTML for archiving, analysis, or migration; export search results or entire threads in one call
39
40
  - 🔍 **Investigate email headers** — check DKIM, SPF, and DMARC authentication; trace delivery chains; analyse spam scores — useful for phishing investigation and compliance
@@ -60,7 +61,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
60
61
 
61
62
  | Module | Tools | What You Can Do |
62
63
  |--------|------:|-----------------|
63
- | **Email** | 7 | `search-emails` (list/search/delta/conversations), `read-email` (content + forensic headers), `send-email` (with dry-run + mail tips), `update-email` (read status, flags), `attachments`, `export`, `get-mail-tips` |
64
+ | **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` |
64
65
  | **Calendar** | 3 | `list-events`, `create-event`, `manage-event` (decline/cancel/delete) |
65
66
  | **Contacts** | 2 | `manage-contact` (list/search/get/create/update/delete), `search-people` |
66
67
  | **Categories** | 3 | `manage-category` (CRUD), `apply-category`, `manage-focused-inbox` |
@@ -70,7 +71,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
70
71
  | **Advanced** | 2 | `access-shared-mailbox`, `find-meeting-rooms` |
71
72
  | **Auth** | 1 | `auth` (status/authenticate/about) |
72
73
 
73
- **21 tools total** — consolidated from 55 for optimal AI performance. See the [Tools Reference](docs/quickrefs/tools-reference.md) for complete parameter details.
74
+ **22 tools total** — consolidated from 55 for optimal AI performance. See the [Tools Reference](docs/quickrefs/tools-reference.md) for complete parameter details.
74
75
 
75
76
  ### Export Formats
76
77
 
@@ -126,7 +127,9 @@ Outlook Assistant is designed with safety-first principles for AI-driven email a
126
127
  - **Session rate limiting** — configurable via `OUTLOOK_MAX_EMAILS_PER_SESSION` (default: unlimited)
127
128
  - **Recipient allowlist** — restrict sending to approved addresses/domains via `OUTLOOK_ALLOWED_RECIPIENTS`
128
129
 
129
- **Token-optimised architecture** — Tools are consolidated using the STRAP (Single Tool, Resource, Action Pattern) approach. 21 tools instead of 55 reduces per-turn overhead by ~11,000 tokens (~64%), keeping more of the AI's context window available for your actual conversation. Fewer tools also means the AI selects the right tool more accurately — research shows tool selection degrades beyond ~40 tools.
130
+ **Draft protections** — The `draft` tool shares `send-email` safety controls: dry-run preview, recipient allowlist, mail-tips validation, and rate limiting. The `send` action shares the `send-email` rate limit counter, preventing circumvention via the draft-then-send pathway.
131
+
132
+ **Token-optimised architecture** — Tools are consolidated using the STRAP (Single Tool, Resource, Action Pattern) approach. 22 tools instead of 55 reduces per-turn overhead by ~11,000 tokens (~64%), keeping more of the AI's context window available for your actual conversation. Fewer tools also means the AI selects the right tool more accurately — research shows tool selection degrades beyond ~40 tools.
130
133
 
131
134
  > **Important**: These safeguards are defence-in-depth measures that reduce risk, but they are not a guarantee against unintended actions. AI-driven access to your email is inherently sensitive — always review tool calls before approving, particularly for sends and deletes. No automated guardrail is foolproof, and you remain responsible for actions taken through your mailbox.
132
135
 
@@ -149,10 +152,11 @@ npx @littlebearapps/outlook-assistant
149
152
  You need a Microsoft Azure app registration to authenticate. See the **[Azure Setup Guide](docs/guides/azure-setup.md)** for a detailed walkthrough (including first-time Azure account creation), or if you've done this before:
150
153
 
151
154
  1. Create a new app registration at [portal.azure.com](https://portal.azure.com/)
152
- 2. Set redirect URI to `http://localhost:3333/auth/callback`
153
- 3. Add Microsoft Graph delegated permissions (Mail, Calendar, Contacts)
154
- 4. Create a client secret and copy the **Value** (not the Secret ID)
155
- 5. Enable **"Allow public client flows"** in Authentication > Advanced settings (for device code flow)
155
+ 2. Add Microsoft Graph delegated permissions (Mail, Calendar, Contacts)
156
+ 3. Create a client secret and copy the **Value** (not the Secret ID)
157
+ 4. Under Authentication > **Add a platform** > **Mobile and desktop applications** — check `nativeclient` URI
158
+ 5. Enable **"Allow public client flows"** in Authentication > Advanced settings
159
+ 6. _(Optional)_ Set redirect URI to `http://localhost:3333/auth/callback` — only needed for browser auth flow
156
160
 
157
161
  ### 3. Configure Your MCP Client
158
162
 
@@ -373,7 +377,7 @@ This starts a local server on port 3333 to handle the OAuth callback.
373
377
 
374
378
  ```
375
379
  outlook-assistant/
376
- ├── index.js # Main entry point (21 tools)
380
+ ├── index.js # Main entry point (22 tools)
377
381
  ├── config.js # Configuration settings
378
382
  ├── outlook-auth-server.js # OAuth server (port 3333)
379
383
  ├── auth/ # Authentication module (1 tool)
@@ -465,7 +469,7 @@ USE_TEST_MODE=true npm start
465
469
  | [Azure Setup Guide](docs/guides/azure-setup.md) | Azure account creation, app registration, permissions, and secrets |
466
470
  | [How-To Guides](docs/how-to/index.md) | 28 practical guides for email, calendar, contacts, and settings |
467
471
  | [Troubleshooting & FAQ](docs/how-to/getting-started/verify-your-connection.md#common-connection-problems) | Common problems, re-authentication, and frequently asked questions |
468
- | [Tools Reference](docs/quickrefs/tools-reference.md) | All 21 tools with parameters |
472
+ | [Tools Reference](docs/quickrefs/tools-reference.md) | All 22 tools with parameters |
469
473
  | [AI Agent Guide](docs/how-to/ai-agents/using-outlook-assistant-in-agents.md) | Tool selection and workflow patterns for AI agents |
470
474
 
471
475
  Full documentation: [docs/](docs/README.md)
package/auth/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  const tokenManager = require('./token-manager');
5
5
  const TokenStorage = require('./token-storage');
6
6
  const config = require('../config');
7
- const { authTools } = require('./tools');
7
+ const { authTools, setToolCount } = require('./tools');
8
8
 
9
9
  // Singleton TokenStorage instance with auto-refresh support
10
10
  const tokenStorage = new TokenStorage({
@@ -39,5 +39,6 @@ module.exports = {
39
39
  tokenManager, // deprecated: use tokenStorage
40
40
  tokenStorage,
41
41
  authTools,
42
+ setToolCount,
42
43
  ensureAuthenticated,
43
44
  };
package/auth/tools.js CHANGED
@@ -5,6 +5,12 @@ const config = require('../config');
5
5
  const tokenManager = require('./token-manager');
6
6
  const { initiateDeviceCodeFlow, pollForToken } = require('./device-code');
7
7
 
8
+ // Dynamic tool count — set by index.js after TOOLS array is built
9
+ let _toolCount = 0;
10
+ function setToolCount(count) {
11
+ _toolCount = count;
12
+ }
13
+
8
14
  /**
9
15
  * About tool handler
10
16
  * @returns {object} - MCP response
@@ -25,7 +31,7 @@ async function handleAbout() {
25
31
  `## Diagnostics\n`,
26
32
  `| Setting | Value |`,
27
33
  `|---------|-------|`,
28
- `| Tools | 20 across 9 modules |`,
34
+ `| Tools | ${_toolCount} across 9 modules |`,
29
35
  `| Modules | auth, email, calendar, folder, rules, contacts, categories, settings, advanced |`,
30
36
  `| Timezone | ${config.DEFAULT_TIMEZONE} |`,
31
37
  `| Test Mode | ${testMode} |`,
@@ -320,6 +326,7 @@ const authTools = [
320
326
 
321
327
  module.exports = {
322
328
  authTools,
329
+ setToolCount,
323
330
  handleAbout,
324
331
  handleAuthenticate,
325
332
  handleDeviceCodeAuth,
@@ -305,17 +305,26 @@ async function handleUpdateCategory(args) {
305
305
  updateData
306
306
  );
307
307
 
308
- const colorName = COLOR_NAMES[response.color] || response.color;
308
+ // Prefer input values over response (PATCH may return partial data)
309
+ const updatedName = displayName || response.displayName;
310
+ const updatedColor = color || response.color;
311
+ const colorName = COLOR_NAMES[updatedColor] || updatedColor;
312
+
313
+ const updatedCategory = {
314
+ ...response,
315
+ displayName: updatedName,
316
+ color: updatedColor,
317
+ };
309
318
 
310
319
  return {
311
320
  content: [
312
321
  {
313
322
  type: 'text',
314
- text: `Category updated!\n\n**Name**: ${response.displayName}\n**Color**: ${colorName} (${response.color})\n**ID**: ${response.id}`,
323
+ text: `Category updated!\n\n**Name**: ${updatedName}\n**Color**: ${colorName} (${updatedColor})\n**ID**: ${response.id || id}`,
315
324
  },
316
325
  ],
317
326
  _meta: {
318
- category: formatCategory(response),
327
+ category: formatCategory(updatedCategory),
319
328
  },
320
329
  };
321
330
  } catch (error) {
@@ -428,7 +437,9 @@ async function handleApplyCategory(args) {
428
437
  };
429
438
  }
430
439
 
431
- if (!categories || !Array.isArray(categories) || categories.length === 0) {
440
+ const applyAction = action || 'set'; // 'set', 'add', 'remove'
441
+
442
+ if (!categories || !Array.isArray(categories)) {
432
443
  return {
433
444
  content: [
434
445
  {
@@ -439,7 +450,17 @@ async function handleApplyCategory(args) {
439
450
  };
440
451
  }
441
452
 
442
- const applyAction = action || 'set'; // 'set', 'add', 'remove'
453
+ // Empty array is only valid for action=set (clears all categories)
454
+ if (categories.length === 0 && applyAction !== 'set') {
455
+ return {
456
+ content: [
457
+ {
458
+ type: 'text',
459
+ text: 'Categories array cannot be empty for add/remove. Use action=set with an empty array to clear all categories.',
460
+ },
461
+ ],
462
+ };
463
+ }
443
464
 
444
465
  try {
445
466
  const accessToken = await ensureAuthenticated();
@@ -17,6 +17,8 @@ const {
17
17
  formatEmailsAsCSV,
18
18
  VERBOSITY,
19
19
  } = require('../utils/response-formatter');
20
+ // Note: buildFromFilter/buildToFilter from search.js use OData $filter which causes
21
+ // InefficientFilter on personal accounts with $orderby. Client-side filtering used instead.
20
22
 
21
23
  /**
22
24
  * Format a date for filenames
@@ -65,10 +67,12 @@ async function handleListConversations(args) {
65
67
  'id',
66
68
  'subject',
67
69
  'from',
70
+ 'toRecipients',
68
71
  'receivedDateTime',
69
72
  'conversationId',
70
73
  'conversationIndex',
71
74
  'isRead',
75
+ 'hasAttachments',
72
76
  'bodyPreview',
73
77
  ].join(',');
74
78
 
@@ -79,6 +83,34 @@ async function handleListConversations(args) {
79
83
  $top: 200, // Get more to group
80
84
  };
81
85
 
86
+ // Apply simple $filter conditions that Graph API supports on personal accounts
87
+ // Complex filters (contains on subject, endswith on email) cause InefficientFilter
88
+ // errors, so those are handled client-side after fetching.
89
+ const serverFilterConditions = [];
90
+ if (args.hasAttachments === true) {
91
+ serverFilterConditions.push('hasAttachments eq true');
92
+ }
93
+ if (args.receivedAfter) {
94
+ try {
95
+ const afterDate = new Date(args.receivedAfter).toISOString();
96
+ serverFilterConditions.push(`receivedDateTime ge ${afterDate}`);
97
+ } catch (_e) {
98
+ /* ignore invalid date */
99
+ }
100
+ }
101
+ if (args.receivedBefore) {
102
+ try {
103
+ const beforeDate = new Date(args.receivedBefore).toISOString();
104
+ serverFilterConditions.push(`receivedDateTime le ${beforeDate}`);
105
+ } catch (_e) {
106
+ /* ignore invalid date */
107
+ }
108
+ }
109
+
110
+ if (serverFilterConditions.length > 0) {
111
+ queryParams.$filter = serverFilterConditions.join(' and ');
112
+ }
113
+
82
114
  const response = await callGraphAPI(
83
115
  accessToken,
84
116
  'GET',
@@ -86,7 +118,33 @@ async function handleListConversations(args) {
86
118
  null,
87
119
  queryParams
88
120
  );
89
- const messages = response.value || [];
121
+ let messages = response.value || [];
122
+
123
+ // Client-side filtering for conditions that cause InefficientFilter on personal accounts
124
+ if (args.subject) {
125
+ const subjectLower = args.subject.toLowerCase();
126
+ messages = messages.filter((m) =>
127
+ (m.subject || '').toLowerCase().includes(subjectLower)
128
+ );
129
+ }
130
+ if (args.from) {
131
+ const fromLower = args.from.toLowerCase();
132
+ messages = messages.filter((m) => {
133
+ const addr = (m.from?.emailAddress?.address || '').toLowerCase();
134
+ const name = (m.from?.emailAddress?.name || '').toLowerCase();
135
+ return addr.includes(fromLower) || name.includes(fromLower);
136
+ });
137
+ }
138
+ if (args.to) {
139
+ const toLower = args.to.toLowerCase();
140
+ messages = messages.filter((m) =>
141
+ (m.toRecipients || []).some((r) => {
142
+ const addr = (r.emailAddress?.address || '').toLowerCase();
143
+ const name = (r.emailAddress?.name || '').toLowerCase();
144
+ return addr.includes(toLower) || name.includes(toLower);
145
+ })
146
+ );
147
+ }
90
148
 
91
149
  // Group by conversationId
92
150
  const conversations = new Map();