@littlebearapps/outlook-assistant 3.13.0 → 3.14.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.
Files changed (67) hide show
  1. package/.env.example +30 -3
  2. package/README.md +67 -27
  3. package/advanced/index.js +44 -174
  4. package/auth/auth-errors.js +23 -1
  5. package/auth/oauth-server.js +7 -1
  6. package/auth/token-manager.js +7 -3
  7. package/auth/token-storage.js +28 -30
  8. package/auth/tools.js +61 -82
  9. package/calendar/attendees.js +36 -0
  10. package/calendar/cancel.js +9 -25
  11. package/calendar/create.js +42 -48
  12. package/calendar/decline.js +10 -25
  13. package/calendar/delete.js +10 -25
  14. package/calendar/index.js +20 -37
  15. package/calendar/list.js +4 -16
  16. package/calendar/preview.js +461 -0
  17. package/calendar/update.js +55 -83
  18. package/categories/index.js +68 -265
  19. package/config.js +29 -1
  20. package/contacts/index.js +72 -128
  21. package/email/attachments.js +43 -125
  22. package/email/conversations.js +44 -78
  23. package/email/delta.js +69 -46
  24. package/email/draft.js +170 -103
  25. package/email/export.js +145 -110
  26. package/email/folder-utils.js +3 -2
  27. package/email/headers.js +11 -49
  28. package/email/index.js +86 -110
  29. package/email/list.js +4 -17
  30. package/email/mail-tips.js +86 -57
  31. package/email/mark-as-read.js +13 -49
  32. package/email/mime.js +39 -51
  33. package/email/read.js +16 -50
  34. package/email/search.js +47 -87
  35. package/email/send.js +82 -48
  36. package/folder/create.js +6 -25
  37. package/folder/delete.js +117 -38
  38. package/folder/index.js +19 -17
  39. package/folder/list.js +5 -17
  40. package/folder/move.js +13 -42
  41. package/folder/resolve.js +11 -6
  42. package/folder/stats.js +18 -27
  43. package/index.js +39 -45
  44. package/llms-install.md +22 -4
  45. package/llms.txt +20 -11
  46. package/outlook-auth-server.js +10 -3
  47. package/package.json +4 -1
  48. package/request-handler.js +217 -116
  49. package/rules/create.js +28 -71
  50. package/rules/index.js +52 -93
  51. package/rules/list.js +7 -19
  52. package/rules/rule-builder.js +59 -22
  53. package/rules/update.js +27 -61
  54. package/server.js +41 -0
  55. package/settings/index.js +162 -145
  56. package/tools.js +30 -0
  57. package/utils/field-presets.js +4 -2
  58. package/utils/graph-api.js +65 -22
  59. package/utils/logger.js +251 -0
  60. package/utils/mock-data.js +91 -2
  61. package/utils/read-only.js +59 -0
  62. package/utils/response-formatter.js +54 -15
  63. package/utils/risk-classes.js +324 -0
  64. package/utils/safe-write.js +372 -6
  65. package/utils/safety.js +247 -42
  66. package/utils/server-instructions.js +73 -0
  67. package/utils/tool-error.js +33 -0
package/folder/delete.js CHANGED
@@ -3,8 +3,108 @@
3
3
  */
4
4
  const { callGraphAPI } = require('../utils/graph-api');
5
5
  const { ensureAuthenticated } = require('../auth');
6
- const { resolveFolder, WELL_KNOWN } = require('./resolve');
6
+ const { resolveFolder, listChildFolders, WELL_KNOWN } = require('./resolve');
7
7
  const { buildMailboxPrefix } = require('../utils/mailbox');
8
+ const { toolError, authRequiredError } = require('../utils/tool-error');
9
+ const { dryRunResult } = require('../utils/safety');
10
+
11
+ const COUNT_FIELDS = 'totalItemCount,unreadItemCount';
12
+ const COUNT_SELECT = `id,displayName,childFolderCount,${COUNT_FIELDS}`;
13
+ /** Most subfolders a dry run counts before reporting "at least". */
14
+ const PREVIEW_FOLDER_LIMIT = 100;
15
+
16
+ const LOSS_NOTE =
17
+ "It doesn't go to Deleted Items, and Graph doesn't document a way to restore it (Outlook's \"Recover deleted items\" may work for a limited time, but don't rely on it). Move out anything you need first.";
18
+
19
+ function plural(count, word) {
20
+ return `${count} ${word}${count === 1 ? '' : 's'}`;
21
+ }
22
+
23
+ /** The folder's quoted path, or its ID if Graph gave it no name. */
24
+ function folderLabel(resolved, quote = "'") {
25
+ return resolved.path
26
+ ? `${quote}${resolved.path}${quote}`
27
+ : `(unnamed, id ${resolved.id})`;
28
+ }
29
+
30
+ /**
31
+ * Count every subfolder below `root`, and the items in them, breadth-first.
32
+ * Stops after PREVIEW_FOLDER_LIMIT subfolders and flags the count partial.
33
+ */
34
+ async function countSubfolders(accessToken, root, mailbox) {
35
+ let subfolders = 0;
36
+ let items = 0;
37
+ let partial = false;
38
+ const queue = root.childFolderCount > 0 ? [root.id] : [];
39
+ while (queue.length > 0) {
40
+ if (subfolders >= PREVIEW_FOLDER_LIMIT) {
41
+ partial = true;
42
+ break;
43
+ }
44
+ const children = await listChildFolders(
45
+ accessToken,
46
+ queue.shift(),
47
+ COUNT_SELECT,
48
+ mailbox
49
+ );
50
+ for (const child of children) {
51
+ subfolders += 1;
52
+ items += child.totalItemCount || 0;
53
+ if (child.childFolderCount > 0) queue.push(child.id);
54
+ }
55
+ }
56
+ return { subfolders, items, partial };
57
+ }
58
+
59
+ /**
60
+ * dryRun preview for delete (#274): the folder, and the items and
61
+ * subfolders that would be lost with it. Reads only, and reads the folder
62
+ * itself only if resolving it didn't already (`resolved.fields`).
63
+ */
64
+ async function previewDeleteFolder(accessToken, resolved, mailbox) {
65
+ const prefix = buildMailboxPrefix(mailbox);
66
+ const folder =
67
+ resolved.fields ||
68
+ (await callGraphAPI(
69
+ accessToken,
70
+ 'GET',
71
+ `${prefix}/mailFolders/${resolved.id}`,
72
+ null,
73
+ { $select: COUNT_SELECT }
74
+ ));
75
+ const items = folder.totalItemCount || 0;
76
+ const unread = folder.unreadItemCount || 0;
77
+ const below = await countSubfolders(
78
+ accessToken,
79
+ { ...folder, id: resolved.id },
80
+ mailbox
81
+ );
82
+ const where = mailbox ? ` in ${mailbox}` : '';
83
+ const name = `${folderLabel(resolved)}${where}`;
84
+
85
+ let summary;
86
+ if (items === 0 && below.subfolders === 0) {
87
+ summary = `Deletes folder ${name}. It's empty: no items or subfolders.`;
88
+ } else {
89
+ const atLeast = below.partial ? 'at least ' : '';
90
+ summary = `Deletes folder ${name} and everything in it: ${plural(items, 'item')}`;
91
+ if (unread > 0) summary += ` (${unread} unread)`;
92
+ if (below.subfolders > 0) {
93
+ summary += `, plus ${atLeast}${plural(below.subfolders, 'subfolder')} holding ${atLeast}${below.items} more ${below.items === 1 ? 'item' : 'items'}`;
94
+ }
95
+ summary += '.';
96
+ }
97
+
98
+ return dryRunResult([summary, LOSS_NOTE], {
99
+ action: 'delete',
100
+ folderId: resolved.id,
101
+ path: resolved.path,
102
+ items,
103
+ subfolders: below.subfolders,
104
+ subfolderItems: below.items,
105
+ partial: below.partial,
106
+ });
107
+ }
8
108
 
9
109
  /**
10
110
  * Delete folder handler
@@ -22,31 +122,19 @@ const { buildMailboxPrefix } = require('../utils/mailbox');
22
122
  * @returns {object} - MCP response
23
123
  */
24
124
  async function handleDeleteFolder(args) {
25
- const { folderId, folderName } = args;
125
+ const { folderId, folderName, dryRun = false } = args;
26
126
  const sharedMailbox = args.sharedMailbox || args.email || null;
27
127
  const prefix = buildMailboxPrefix(sharedMailbox);
28
128
 
29
129
  if (!folderId && !folderName) {
30
- return {
31
- content: [
32
- {
33
- type: 'text',
34
- text: 'Either folderId or folderName is required.',
35
- },
36
- ],
37
- };
130
+ return toolError('Either folderId or folderName is required.');
38
131
  }
39
132
 
40
133
  // Name/alias guard for the common accidental case.
41
134
  if (folderName && WELL_KNOWN[folderName.toLowerCase().trim()]) {
42
- return {
43
- content: [
44
- {
45
- type: 'text',
46
- text: `Cannot delete protected folder "${folderName}". Protected folders: Inbox, Drafts, Sent Items, Deleted Items, Junk Email, Archive, Outbox.`,
47
- },
48
- ],
49
- };
135
+ return toolError(
136
+ `Cannot delete protected folder "${folderName}". Protected folders: Inbox, Drafts, Sent Items, Deleted Items, Junk Email, Archive, Outbox.`
137
+ );
50
138
  }
51
139
 
52
140
  try {
@@ -60,11 +148,16 @@ async function handleDeleteFolder(args) {
60
148
  id: folderId,
61
149
  name: folderName,
62
150
  mailbox: sharedMailbox,
151
+ // A dry run by ID reads the counts in the same request.
152
+ ...(dryRun && { extraSelect: COUNT_FIELDS }),
63
153
  });
64
154
  } catch (resolveError) {
65
- return {
66
- content: [{ type: 'text', text: resolveError.message }],
67
- };
155
+ return toolError(resolveError.message);
156
+ }
157
+
158
+ // dryRun: say what would be lost; delete nothing.
159
+ if (dryRun) {
160
+ return await previewDeleteFolder(accessToken, resolved, sharedMailbox);
68
161
  }
69
162
 
70
163
  // Delete the folder
@@ -77,29 +170,15 @@ async function handleDeleteFolder(args) {
77
170
  content: [
78
171
  {
79
172
  type: 'text',
80
- text: `Folder "${resolved.path}" deleted successfully.`,
173
+ text: `Folder ${folderLabel(resolved, '"')} deleted successfully.`,
81
174
  },
82
175
  ],
83
176
  };
84
177
  } catch (error) {
85
178
  if (error.message === 'Authentication required') {
86
- return {
87
- content: [
88
- {
89
- type: 'text',
90
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
91
- },
92
- ],
93
- };
179
+ return authRequiredError();
94
180
  }
95
- return {
96
- content: [
97
- {
98
- type: 'text',
99
- text: `Error deleting folder: ${error.message}`,
100
- },
101
- ],
102
- };
181
+ return toolError(`Error deleting folder: ${error.message}`);
103
182
  }
104
183
  }
105
184
 
package/folder/index.js CHANGED
@@ -6,19 +6,17 @@ const handleCreateFolder = require('./create');
6
6
  const handleMoveEmails = require('./move');
7
7
  const handleGetFolderStats = require('./stats');
8
8
  const handleDeleteFolder = require('./delete');
9
+ const { toolMetadata } = require('../utils/risk-classes');
10
+ const { toolError } = require('../utils/tool-error');
11
+ const { dryRunUnsupported } = require('../utils/safety');
9
12
 
10
13
  // Consolidated folder tool definition
11
14
  const folderTools = [
12
15
  {
13
16
  name: 'folders',
14
17
  description:
15
- "Manage mail folders (tool-level destructiveHint=true because `delete` removes a folder and its contents; `list` and `stats` are read-only sub-actions despite the annotation). Folders can be addressed by name, by a slash-separated PATH for nested folders (e.g. `Triage/Delete`, `Inbox/Clients/Acme`, case-insensitive), or by explicit ID; `list` output includes each folder's full path and `[id: …]`. A bare name resolves a unique top-level folder first, then searches nested folders (ambiguous names return the candidates — disambiguate with a path or ID). action=`list` (default) returns the folder tree (toggle `includeItemCounts` for unread/total, `includeChildren` for hierarchy). action=`create` makes a new folder under the root, or under `parentFolder` (name/path) / `parentFolderId`, and returns its id. action=`move` relocates emails (`emailIds`) into `targetFolder` (name/path) or `targetFolderId`. action=`stats` returns counts (totalItemCount/unreadItemCount) for `folder` (name/path) or `folderId`, suitable for pagination planning. action=`delete` removes a folder (by `folderName`/path or `folderId`) and its contents — it does not go to Deleted Items, and Graph doesn't document whether it can be restored (some accounts may offer Outlook's \"Recover deleted items\" for a limited time, but don't rely on it), so move out anything you might need first. Every action accepts `sharedMailbox` (alias `email`) to target a shared/delegated mailbox instead of the signed-in account — folder names, paths, and IDs are then resolved inside that mailbox. Protected folders cannot be deleted in any mailbox.",
16
- annotations: {
17
- title: 'Mail Folders',
18
- readOnlyHint: false,
19
- destructiveHint: true,
20
- openWorldHint: false,
21
- },
18
+ "Manage mail folders. Address a folder by name, by slash-separated path for nested folders (e.g. `Inbox/Clients/Acme`, case-insensitive) or by ID; a bare name matches a unique top-level folder first, then nested ones (an ambiguous name returns the candidates). action=`list` (default) returns the tree with each folder's path and id (`includeItemCounts`, `includeChildren`). action=`create` makes `name` under the root or `parentFolder`/`parentFolderId`. action=`move` moves `emailIds` into `targetFolder`/`targetFolderId`. action=`stats` returns total/unread counts for `folder` or `folderId`. action=`delete` removes a folder (`folderName`/path or `folderId`) with everything in it, subfolders included. It skips Deleted Items and Graph documents no restore path, so pass `dryRun: true` first to see what would be lost. Protected folders (Inbox, Sent Items, etc.) can't be deleted. Every action accepts `sharedMailbox` (alias `email`) to work in a shared or delegated mailbox (default: your own).",
19
+ ...toolMetadata('folders', 'Mail Folders'),
22
20
  inputSchema: {
23
21
  type: 'object',
24
22
  properties: {
@@ -40,7 +38,7 @@ const folderTools = [
40
38
  sharedMailbox: {
41
39
  type: 'string',
42
40
  description:
43
- 'Email address of a shared/delegated mailbox to target instead of the signed-in account (all actions). Requires delegate access + Mail.Read.Shared (list/stats) or Mail.ReadWrite.Shared (create/move/delete). Work/school only; needs the server opt-in setting OUTLOOK_SHARED_MAILBOX (otherwise the call is refused with setup guidance).',
41
+ 'Email address of a shared/delegated mailbox to target (all actions; default: the signed-in account). Requires delegate access + Mail.Read.Shared (list/stats) or Mail.ReadWrite.Shared (create/move/delete). Work/school only; needs the server opt-in setting OUTLOOK_SHARED_MAILBOX (otherwise the call is refused with setup guidance).',
44
42
  },
45
43
  email: {
46
44
  type: 'string',
@@ -79,7 +77,8 @@ const folderTools = [
79
77
  },
80
78
  sourceFolder: {
81
79
  type: 'string',
82
- description: 'Source folder name, default is inbox (action=move)',
80
+ description:
81
+ 'Ignored: action=move moves each email by ID from wherever it is. Accepted for older callers.',
83
82
  },
84
83
  // stats params
85
84
  folder: {
@@ -102,12 +101,20 @@ const folderTools = [
102
101
  description:
103
102
  'Folder name or path to delete — resolved to ID (action=delete). Cannot delete protected folders (Inbox, Drafts, Sent, etc.)',
104
103
  },
104
+ dryRun: {
105
+ type: 'boolean',
106
+ description:
107
+ 'Preview only (action=delete): nothing is deleted. Shows the folder and how many items and subfolders would be lost. Other actions refuse dryRun and change nothing. Default false.',
108
+ },
105
109
  },
106
110
  additionalProperties: false,
107
111
  required: [],
108
112
  },
109
113
  handler: async (args) => {
110
114
  const action = args.action || 'list';
115
+ if (args.dryRun && action !== 'delete') {
116
+ return dryRunUnsupported('folders', action, 'delete');
117
+ }
111
118
  switch (action) {
112
119
  case 'create':
113
120
  return handleCreateFolder(args);
@@ -120,14 +127,9 @@ const folderTools = [
120
127
  case 'list':
121
128
  return handleListFolders(args);
122
129
  default:
123
- return {
124
- content: [
125
- {
126
- type: 'text',
127
- text: `Unknown action '${action}'. Valid actions: list, create, move, stats, delete.`,
128
- },
129
- ],
130
- };
130
+ return toolError(
131
+ `Unknown action '${action}'. Valid actions: list, create, move, stats, delete.`
132
+ );
131
133
  }
132
134
  },
133
135
  },
package/folder/list.js CHANGED
@@ -3,6 +3,8 @@
3
3
  */
4
4
  const { ensureAuthenticated } = require('../auth');
5
5
  const { listChildFolders } = require('./resolve');
6
+ const { toolError, authRequiredError } = require('../utils/tool-error');
7
+ const { log } = require('../utils/logger');
6
8
 
7
9
  /**
8
10
  * List folders handler
@@ -52,24 +54,10 @@ async function handleListFolders(args) {
52
54
  };
53
55
  } catch (error) {
54
56
  if (error.message === 'Authentication required') {
55
- return {
56
- content: [
57
- {
58
- type: 'text',
59
- text: "Authentication required. Please use the 'authenticate' tool first.",
60
- },
61
- ],
62
- };
57
+ return authRequiredError();
63
58
  }
64
59
 
65
- return {
66
- content: [
67
- {
68
- type: 'text',
69
- text: `Error listing folders: ${error.message}`,
70
- },
71
- ],
72
- };
60
+ return toolError(`Error listing folders: ${error.message}`);
73
61
  }
74
62
  }
75
63
 
@@ -133,7 +121,7 @@ async function getAllFoldersHierarchy(
133
121
  sharedMailbox
134
122
  );
135
123
  } catch (error) {
136
- console.error(
124
+ log.debug(
137
125
  `Error getting child folders for "${folder.displayName}": ${error.message}`
138
126
  );
139
127
  warnings.push(
package/folder/move.js CHANGED
@@ -5,6 +5,8 @@ const { callGraphAPI } = require('../utils/graph-api');
5
5
  const { ensureAuthenticated } = require('../auth');
6
6
  const { resolveFolder } = require('./resolve');
7
7
  const { buildMailboxPrefix } = require('../utils/mailbox');
8
+ const { toolError, authRequiredError } = require('../utils/tool-error');
9
+ const { log } = require('../utils/logger');
8
10
 
9
11
  /**
10
12
  * Move emails handler
@@ -19,25 +21,15 @@ async function handleMoveEmails(args) {
19
21
  const sharedMailbox = args.sharedMailbox || args.email || null;
20
22
 
21
23
  if (!emailIds) {
22
- return {
23
- content: [
24
- {
25
- type: 'text',
26
- text: 'Email IDs are required. Please provide a comma-separated list of email IDs to move.',
27
- },
28
- ],
29
- };
24
+ return toolError(
25
+ 'Email IDs are required. Please provide a comma-separated list of email IDs to move.'
26
+ );
30
27
  }
31
28
 
32
29
  if (!targetFolder && !targetFolderId) {
33
- return {
34
- content: [
35
- {
36
- type: 'text',
37
- text: 'Target folder is required — pass `targetFolder` (name or "Parent/Child" path) or `targetFolderId`.',
38
- },
39
- ],
40
- };
30
+ return toolError(
31
+ 'Target folder is required — pass `targetFolder` (name or "Parent/Child" path) or `targetFolderId`.'
32
+ );
41
33
  }
42
34
 
43
35
  try {
@@ -51,14 +43,7 @@ async function handleMoveEmails(args) {
51
43
  .filter((id) => id);
52
44
 
53
45
  if (ids.length === 0) {
54
- return {
55
- content: [
56
- {
57
- type: 'text',
58
- text: 'No valid email IDs provided.',
59
- },
60
- ],
61
- };
46
+ return toolError('No valid email IDs provided.');
62
47
  }
63
48
 
64
49
  // Move emails
@@ -85,24 +70,10 @@ async function handleMoveEmails(args) {
85
70
  };
86
71
  } catch (error) {
87
72
  if (error.message === 'Authentication required') {
88
- return {
89
- content: [
90
- {
91
- type: 'text',
92
- text: "Authentication required. Please use the 'authenticate' tool first.",
93
- },
94
- ],
95
- };
73
+ return authRequiredError();
96
74
  }
97
75
 
98
- return {
99
- content: [
100
- {
101
- type: 'text',
102
- text: `Error moving emails: ${error.message}`,
103
- },
104
- ],
105
- };
76
+ return toolError(`Error moving emails: ${error.message}`);
106
77
  }
107
78
  }
108
79
 
@@ -157,7 +128,7 @@ async function moveEmailsToFolder(
157
128
  newId: moved?.id || emailId,
158
129
  });
159
130
  } catch (error) {
160
- console.error(`Error moving email ${emailId}: ${error.message}`);
131
+ log.debug(`Error moving email ${emailId}: ${error.message}`);
161
132
  results.failed.push({
162
133
  id: emailId,
163
134
  error: error.message,
@@ -203,7 +174,7 @@ async function moveEmailsToFolder(
203
174
  results,
204
175
  };
205
176
  } catch (error) {
206
- console.error(`Error in moveEmailsToFolder: ${error.message}`);
177
+ log.debug(`Error in moveEmailsToFolder: ${error.message}`);
207
178
  throw error;
208
179
  }
209
180
  }
package/folder/resolve.js CHANGED
@@ -132,15 +132,18 @@ async function resolveWellKnown(accessToken, alias, mailbox) {
132
132
  return toRecord(resp, resp.displayName);
133
133
  }
134
134
 
135
- async function resolveById(accessToken, id, mailbox) {
135
+ async function resolveById(accessToken, id, mailbox, extraSelect) {
136
136
  const resp = await callGraphAPI(
137
137
  accessToken,
138
138
  'GET',
139
139
  `${buildMailboxPrefix(mailbox)}/mailFolders/${id}`,
140
140
  null,
141
- { $select: FOLDER_SELECT }
141
+ { $select: extraSelect ? `${FOLDER_SELECT},${extraSelect}` : FOLDER_SELECT }
142
142
  );
143
- return toRecord(resp, resp.displayName);
143
+ // The ID asked for stands in if a response leaves `id` out.
144
+ const folder = { ...resp, id: resp?.id || id };
145
+ const record = toRecord(folder, folder.displayName);
146
+ return extraSelect ? { ...record, fields: folder } : record;
144
147
  }
145
148
 
146
149
  /**
@@ -300,8 +303,10 @@ function looksLikeFolderId(value) {
300
303
  /**
301
304
  * Resolve a folder from a name/path and/or explicit ID.
302
305
  * @param {string} accessToken
303
- * @param {{name?: string, id?: string, mailbox?: string|null}} spec
304
- * @returns {Promise<{id: string, displayName: string, parentId: string|null, path: string}>}
306
+ * @param {{name?: string, id?: string, mailbox?: string|null, extraSelect?: string}} spec
307
+ * `extraSelect`: more fields to read when resolving by ID (e.g. item
308
+ * counts), returned as `fields` so the caller needn't read the folder again
309
+ * @returns {Promise<{id: string, displayName: string, parentId: string|null, path: string, fields?: object}>}
305
310
  * `path` is the full slash-separated path when resolved by name/path/alias;
306
311
  * when resolved by ID it is the folder's display name only (ancestors are not
307
312
  * fetched).
@@ -310,7 +315,7 @@ async function resolveFolder(accessToken, spec = {}) {
310
315
  const mailbox = spec.mailbox || null;
311
316
  const id = (spec.id || '').trim();
312
317
  if (id) {
313
- return resolveById(accessToken, id, mailbox);
318
+ return resolveById(accessToken, id, mailbox, spec.extraSelect);
314
319
  }
315
320
  const name = (spec.name || '').trim();
316
321
  if (!name) {
package/folder/stats.js CHANGED
@@ -9,6 +9,8 @@ const { ensureAuthenticated } = require('../auth');
9
9
  const { resolveFolder } = require('./resolve');
10
10
  const { buildMailboxPrefix } = require('../utils/mailbox');
11
11
  const config = require('../config');
12
+ const { toolError, authRequiredError } = require('../utils/tool-error');
13
+ const { log } = require('../utils/logger');
12
14
 
13
15
  const { VERBOSITY, DEFAULT_LIMITS } = config;
14
16
 
@@ -36,9 +38,7 @@ async function handleGetFolderStats(args) {
36
38
  mailbox: sharedMailbox,
37
39
  });
38
40
  } catch (resolveError) {
39
- return {
40
- content: [{ type: 'text', text: resolveError.message }],
41
- };
41
+ return toolError(resolveError.message);
42
42
  }
43
43
  const folderId = resolved.id;
44
44
 
@@ -75,24 +75,10 @@ async function handleGetFolderStats(args) {
75
75
  };
76
76
  } catch (error) {
77
77
  if (error.message === 'Authentication required') {
78
- return {
79
- content: [
80
- {
81
- type: 'text',
82
- text: "Authentication required. Please use the 'authenticate' tool first.",
83
- },
84
- ],
85
- };
78
+ return authRequiredError();
86
79
  }
87
80
 
88
- return {
89
- content: [
90
- {
91
- type: 'text',
92
- text: `Error getting folder stats: ${error.message}`,
93
- },
94
- ],
95
- };
81
+ return toolError(`Error getting folder stats: ${error.message}`);
96
82
  }
97
83
  }
98
84
 
@@ -139,7 +125,7 @@ async function getEmailDateRange(accessToken, folderId, mailbox = null) {
139
125
  return { newest, oldest };
140
126
  }
141
127
  } catch (error) {
142
- console.error(`Error getting date range: ${error.message}`);
128
+ log.debug(`Error getting date range: ${error.message}`);
143
129
  }
144
130
 
145
131
  return null;
@@ -194,8 +180,10 @@ function formatFolderStats(folder, dateRange, verbosity) {
194
180
  text += `| Date Range | ${oldest} to ${newest} |\n`;
195
181
  }
196
182
 
197
- if (totalItems > 100) {
198
- text += `\n_Hint: Use list-emails-delta for efficient incremental sync of large folders._`;
183
+ // List mode returns at most 50 with no page cursor, so anything bigger
184
+ // needs delta sync to read in full.
185
+ if (totalItems > 50) {
186
+ text += `\n_Hint: Use \`search-emails\` with \`deltaMode: true\` for efficient incremental sync of large folders._`;
199
187
  }
200
188
 
201
189
  return { text, meta };
@@ -224,7 +212,10 @@ function formatFolderStats(folder, dateRange, verbosity) {
224
212
  text += `|---------|-------|\n`;
225
213
  text += `| Page Size | ${pageSize} emails |\n`;
226
214
  text += `| Total Pages | ${totalPages} |\n`;
227
- text += `| Estimated API Calls | ${totalPages} (list-emails) |\n`;
215
+ text +=
216
+ totalItems > 50
217
+ ? `| Estimated API Calls | ${Math.ceil(totalItems / 100)} (\`search-emails\` \`deltaMode: true\`, 100 per page; list mode stops at 50) |\n`
218
+ : `| Estimated API Calls | 1 (\`search-emails\` list mode, \`count\` up to 50) |\n`;
228
219
 
229
220
  if (dateRange) {
230
221
  const newestDate = new Date(dateRange.newest);
@@ -247,12 +238,12 @@ function formatFolderStats(folder, dateRange, verbosity) {
247
238
  text += `\n## Recommendations\n\n`;
248
239
 
249
240
  if (totalItems > 1000) {
250
- text += `- **Large folder**: Use \`list-emails-delta\` for incremental sync\n`;
241
+ text += `- **Large folder**: Use \`search-emails\` with \`deltaMode: true\` for incremental sync\n`;
251
242
  text += `- **Use date filters**: \`receivedAfter\` and \`receivedBefore\` to narrow scope\n`;
252
- } else if (totalItems > 100) {
253
- text += `- **Medium folder**: Consider using \`list-emails-delta\` for efficient updates\n`;
243
+ } else if (totalItems > 50) {
244
+ text += `- **Medium folder**: Consider \`search-emails\` with \`deltaMode: true\` for efficient updates\n`;
254
245
  } else {
255
- text += `- **Small folder**: \`list-emails\` with default pagination is efficient\n`;
246
+ text += `- **Small folder**: \`search-emails\` in list mode (raise \`count\` up to 50) is enough\n`;
256
247
  }
257
248
 
258
249
  if (unreadItems > 50) {