@littlebearapps/outlook-assistant 3.12.1 → 3.14.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.
Files changed (69) hide show
  1. package/.env.example +27 -3
  2. package/README.md +108 -33
  3. package/advanced/index.js +44 -174
  4. package/auth/auth-errors.js +23 -1
  5. package/auth/client-config.js +142 -0
  6. package/auth/index.js +4 -2
  7. package/auth/oauth-server.js +12 -2
  8. package/auth/token-manager.js +7 -3
  9. package/auth/token-storage.js +46 -33
  10. package/auth/tools.js +223 -93
  11. package/calendar/attendees.js +36 -0
  12. package/calendar/cancel.js +9 -25
  13. package/calendar/create.js +42 -48
  14. package/calendar/decline.js +10 -25
  15. package/calendar/delete.js +10 -25
  16. package/calendar/index.js +20 -37
  17. package/calendar/list.js +4 -16
  18. package/calendar/preview.js +335 -0
  19. package/calendar/update.js +42 -86
  20. package/categories/index.js +59 -264
  21. package/config.js +36 -2
  22. package/contacts/index.js +72 -128
  23. package/email/attachments.js +42 -124
  24. package/email/conversations.js +44 -78
  25. package/email/delta.js +10 -34
  26. package/email/draft.js +140 -96
  27. package/email/export.js +141 -110
  28. package/email/folder-utils.js +3 -2
  29. package/email/headers.js +11 -49
  30. package/email/index.js +85 -109
  31. package/email/list.js +4 -17
  32. package/email/mail-tips.js +86 -57
  33. package/email/mark-as-read.js +13 -49
  34. package/email/mime.js +14 -49
  35. package/email/read.js +16 -50
  36. package/email/search.js +46 -86
  37. package/email/send.js +82 -48
  38. package/folder/create.js +6 -25
  39. package/folder/delete.js +117 -38
  40. package/folder/index.js +17 -16
  41. package/folder/list.js +5 -17
  42. package/folder/move.js +13 -42
  43. package/folder/resolve.js +11 -6
  44. package/folder/stats.js +6 -20
  45. package/index.js +23 -45
  46. package/llms-install.md +31 -7
  47. package/llms.txt +19 -10
  48. package/outlook-auth-server.js +10 -3
  49. package/package.json +6 -2
  50. package/request-handler.js +217 -116
  51. package/rules/create.js +27 -70
  52. package/rules/index.js +30 -92
  53. package/rules/list.js +5 -17
  54. package/rules/rule-builder.js +57 -20
  55. package/rules/update.js +26 -60
  56. package/server.js +37 -0
  57. package/settings/index.js +142 -143
  58. package/tools.js +30 -0
  59. package/utils/field-presets.js +4 -2
  60. package/utils/graph-api.js +65 -22
  61. package/utils/logger.js +251 -0
  62. package/utils/mock-data.js +91 -2
  63. package/utils/read-only.js +59 -0
  64. package/utils/response-formatter.js +54 -15
  65. package/utils/risk-classes.js +324 -0
  66. package/utils/safe-write.js +372 -6
  67. package/utils/safety.js +109 -25
  68. package/utils/server-instructions.js +62 -0
  69. 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',
@@ -102,12 +100,20 @@ const folderTools = [
102
100
  description:
103
101
  'Folder name or path to delete — resolved to ID (action=delete). Cannot delete protected folders (Inbox, Drafts, Sent, etc.)',
104
102
  },
103
+ dryRun: {
104
+ type: 'boolean',
105
+ description:
106
+ '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.',
107
+ },
105
108
  },
106
109
  additionalProperties: false,
107
110
  required: [],
108
111
  },
109
112
  handler: async (args) => {
110
113
  const action = args.action || 'list';
114
+ if (args.dryRun && action !== 'delete') {
115
+ return dryRunUnsupported('folders', action, 'delete');
116
+ }
111
117
  switch (action) {
112
118
  case 'create':
113
119
  return handleCreateFolder(args);
@@ -120,14 +126,9 @@ const folderTools = [
120
126
  case 'list':
121
127
  return handleListFolders(args);
122
128
  default:
123
- return {
124
- content: [
125
- {
126
- type: 'text',
127
- text: `Unknown action '${action}'. Valid actions: list, create, move, stats, delete.`,
128
- },
129
- ],
130
- };
129
+ return toolError(
130
+ `Unknown action '${action}'. Valid actions: list, create, move, stats, delete.`
131
+ );
131
132
  }
132
133
  },
133
134
  },
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;
package/index.js CHANGED
@@ -31,11 +31,15 @@ stdio. It is normally launched by an MCP client (Claude Desktop, Claude Code)
31
31
  rather than run by hand — started from a terminal it will simply wait on stdin.
32
32
 
33
33
  Key environment variables:
34
- OUTLOOK_CLIENT_ID Azure app registration client ID
35
- OUTLOOK_CLIENT_SECRET Client secret VALUE (not the Secret ID)
34
+ OUTLOOK_CLIENT_ID Azure Application (client) ID. If you can't set env
35
+ vars, pass it to the auth tool instead (action=authenticate
36
+ clientId=<id>); it's saved to ~/.outlook-assistant-config.json
37
+ OUTLOOK_CLIENT_SECRET Client secret VALUE (not the Secret ID); browser flow only
36
38
  OUTLOOK_AUTH_METHOD device-code (default) | browser
37
39
  OUTLOOK_AUTH_AUDIENCE common | consumers | organizations | <tenant-guid>
38
40
  OUTLOOK_SHARED_MAILBOX Opt in to shared mailboxes: read | true (work/school only)
41
+ OUTLOOK_READ_ONLY Set to "true" to refuse every tool call that would change,
42
+ send or delete anything (reads and sign-in still work)
39
43
  OUTLOOK_ALLOWED_RECIPIENTS Comma-separated recipient allowlist
40
44
  OUTLOOK_MAX_EMAILS_PER_SESSION Default cap per session for every rate-limited tool
41
45
  (send-email, draft, manage-rules); 0 or unset = no cap
@@ -45,6 +49,9 @@ Key environment variables:
45
49
  OUTLOOK_IMMUTABLE_IDS Set to "true" for message IDs that survive folder moves
46
50
  OUTLOOK_SEARCH_SCAN_LIMIT Local search fallback window (default 500, max 5000)
47
51
  OUTLOOK_REQUEST_TIMEOUT_MS Graph request inactivity timeout (default 60000)
52
+ OUTLOOK_DEBUG Set to "true" for detailed stderr logs (addresses redacted)
53
+ OUTLOOK_EXPORT_DIR Extra folder export/attachment downloads may write to
54
+ (besides the temp directory, ~/Downloads, ~/Documents)
48
55
  USE_TEST_MODE Set to "true" to run against mock data
49
56
 
50
57
  Documentation: https://github.com/littlebearapps/outlook-assistant`;
@@ -72,27 +79,26 @@ Documentation: https://github.com/littlebearapps/outlook-assistant`;
72
79
  process.exit(0);
73
80
  }
74
81
 
75
- const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
76
82
  const {
77
83
  StdioServerTransport,
78
84
  } = require('@modelcontextprotocol/sdk/server/stdio.js');
79
85
  const config = require('./config');
80
- const { createRequestHandler } = require('./request-handler');
81
-
82
- // Import module tools
83
- const { authTools, setToolCount } = require('./auth');
84
- const { calendarTools } = require('./calendar');
85
- const { emailTools } = require('./email');
86
- const { folderTools } = require('./folder');
87
- const { rulesTools } = require('./rules');
88
- const { contactsTools } = require('./contacts');
89
- const { categoriesTools } = require('./categories');
90
- const { settingsTools } = require('./settings');
91
- const { advancedTools } = require('./advanced');
86
+ const { createServer } = require('./server');
87
+
88
+ const { setToolCount } = require('./auth');
89
+ const { TOOLS } = require('./tools');
90
+ const { isDebugEnabled } = require('./utils/logger');
92
91
 
93
92
  // Log startup information
94
- console.error(`STARTING ${config.SERVER_NAME.toUpperCase()} MCP SERVER`);
93
+ console.error(
94
+ `STARTING ${config.SERVER_NAME.toUpperCase()} MCP SERVER v${config.SERVER_VERSION}`
95
+ );
95
96
  console.error(`Test mode is ${config.USE_TEST_MODE ? 'enabled' : 'disabled'}`);
97
+ if (isDebugEnabled()) {
98
+ console.error(
99
+ 'Debug logging is on (OUTLOOK_DEBUG): stderr includes search terms, subjects and Graph errors, with addresses and IDs redacted.'
100
+ );
101
+ }
96
102
 
97
103
  // F-1 / F-48: warn at startup when safety belts are unset. Mirrors the
98
104
  // warning surfaced by `auth action=about`. Visible to operators reading
@@ -107,38 +113,10 @@ if (
107
113
  );
108
114
  }
109
115
 
110
- // Combine all tools
111
- const TOOLS = [
112
- ...authTools,
113
- ...calendarTools,
114
- ...emailTools,
115
- ...folderTools,
116
- ...rulesTools,
117
- ...contactsTools,
118
- ...categoriesTools,
119
- ...settingsTools,
120
- ...advancedTools,
121
- ];
122
-
123
116
  // Set dynamic tool count for auth about handler
124
117
  setToolCount(TOOLS.length);
125
118
 
126
- // Create server with tools capabilities
127
- const server = new Server(
128
- { name: config.SERVER_NAME, version: config.SERVER_VERSION },
129
- {
130
- capabilities: {
131
- tools: TOOLS.reduce((acc, tool) => {
132
- acc[tool.name] = {};
133
- return acc;
134
- }, {}),
135
- },
136
- }
137
- );
138
-
139
- // Handle all requests. Dispatch + error-shaping logic lives in
140
- // request-handler.js so it is unit-testable without starting the transport.
141
- server.fallbackRequestHandler = createRequestHandler(TOOLS);
119
+ const server = createServer(TOOLS);
142
120
 
143
121
  // Make the script executable
144
122
  process.on('SIGTERM', () => {