@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/contacts/index.js CHANGED
@@ -9,6 +9,10 @@ const {
9
9
  } = require('../utils/graph-api');
10
10
  const { ensureAuthenticated } = require('../auth');
11
11
  const { quoteSearchPhrase } = require('../utils/odata-helpers');
12
+ const { toolMetadata } = require('../utils/risk-classes');
13
+ const { toolError, authRequiredError } = require('../utils/tool-error');
14
+ const { dryRunResult, dryRunUnsupported } = require('../utils/safety');
15
+ const { log } = require('../utils/logger');
12
16
 
13
17
  /**
14
18
  * Contact field presets for different use cases
@@ -195,20 +199,9 @@ async function handleListContacts(args) {
195
199
  };
196
200
  } catch (error) {
197
201
  if (error.message === 'Authentication required') {
198
- return {
199
- content: [
200
- {
201
- type: 'text',
202
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
203
- },
204
- ],
205
- };
202
+ return authRequiredError();
206
203
  }
207
- return {
208
- content: [
209
- { type: 'text', text: `Error listing contacts: ${error.message}` },
210
- ],
211
- };
204
+ return toolError(`Error listing contacts: ${error.message}`);
212
205
  }
213
206
  }
214
207
 
@@ -221,7 +214,7 @@ async function handleSearchContacts(args) {
221
214
  const verbosity = args.outputVerbosity || 'standard';
222
215
 
223
216
  if (!query) {
224
- return { content: [{ type: 'text', text: 'Search query is required.' }] };
217
+ return toolError('Search query is required.');
225
218
  }
226
219
 
227
220
  try {
@@ -252,7 +245,7 @@ async function handleSearchContacts(args) {
252
245
  );
253
246
  } catch (filterError) {
254
247
  // Fallback: fetch all contacts and filter client-side if $filter is unsupported
255
- console.error(
248
+ log.debug(
256
249
  `Contact $filter failed (${filterError.message}), falling back to client-side filter`
257
250
  );
258
251
  const allParams = {
@@ -295,20 +288,9 @@ async function handleSearchContacts(args) {
295
288
  };
296
289
  } catch (error) {
297
290
  if (error.message === 'Authentication required') {
298
- return {
299
- content: [
300
- {
301
- type: 'text',
302
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
303
- },
304
- ],
305
- };
291
+ return authRequiredError();
306
292
  }
307
- return {
308
- content: [
309
- { type: 'text', text: `Error searching contacts: ${error.message}` },
310
- ],
311
- };
293
+ return toolError(`Error searching contacts: ${error.message}`);
312
294
  }
313
295
  }
314
296
 
@@ -319,7 +301,7 @@ async function handleGetContact(args) {
319
301
  const contactId = args.id;
320
302
 
321
303
  if (!contactId) {
322
- return { content: [{ type: 'text', text: 'Contact ID is required.' }] };
304
+ return toolError('Contact ID is required.');
323
305
  }
324
306
 
325
307
  try {
@@ -346,20 +328,9 @@ async function handleGetContact(args) {
346
328
  };
347
329
  } catch (error) {
348
330
  if (error.message === 'Authentication required') {
349
- return {
350
- content: [
351
- {
352
- type: 'text',
353
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
354
- },
355
- ],
356
- };
331
+ return authRequiredError();
357
332
  }
358
- return {
359
- content: [
360
- { type: 'text', text: `Error getting contact: ${error.message}` },
361
- ],
362
- };
333
+ return toolError(`Error getting contact: ${error.message}`);
363
334
  }
364
335
  }
365
336
 
@@ -387,14 +358,9 @@ async function handleCreateContact(args) {
387
358
  (Array.isArray(emails) && emails[0]);
388
359
 
389
360
  if (!resolvedDisplayName && !email && !(emails && emails.length > 0)) {
390
- return {
391
- content: [
392
- {
393
- type: 'text',
394
- text: 'At least displayName, firstName/lastName, email, or emails is required.',
395
- },
396
- ],
397
- };
361
+ return toolError(
362
+ 'At least displayName, firstName/lastName, email, or emails is required.'
363
+ );
398
364
  }
399
365
 
400
366
  try {
@@ -453,20 +419,9 @@ async function handleCreateContact(args) {
453
419
  };
454
420
  } catch (error) {
455
421
  if (error.message === 'Authentication required') {
456
- return {
457
- content: [
458
- {
459
- type: 'text',
460
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
461
- },
462
- ],
463
- };
422
+ return authRequiredError();
464
423
  }
465
- return {
466
- content: [
467
- { type: 'text', text: `Error creating contact: ${error.message}` },
468
- ],
469
- };
424
+ return toolError(`Error creating contact: ${error.message}`);
470
425
  }
471
426
  }
472
427
 
@@ -478,7 +433,7 @@ async function handleUpdateContact(args) {
478
433
  args;
479
434
 
480
435
  if (!id) {
481
- return { content: [{ type: 'text', text: 'Contact ID is required.' }] };
436
+ return toolError('Contact ID is required.');
482
437
  }
483
438
 
484
439
  try {
@@ -521,23 +476,37 @@ async function handleUpdateContact(args) {
521
476
  };
522
477
  } catch (error) {
523
478
  if (error.message === 'Authentication required') {
524
- return {
525
- content: [
526
- {
527
- type: 'text',
528
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
529
- },
530
- ],
531
- };
479
+ return authRequiredError();
532
480
  }
533
- return {
534
- content: [
535
- { type: 'text', text: `Error updating contact: ${error.message}` },
536
- ],
537
- };
481
+ return toolError(`Error updating contact: ${error.message}`);
538
482
  }
539
483
  }
540
484
 
485
+ /**
486
+ * dryRun preview for delete (#274): which contact would be lost. Reads only.
487
+ */
488
+ async function previewDeleteContact(accessToken, contactId) {
489
+ const contact = await callGraphAPI(
490
+ accessToken,
491
+ 'GET',
492
+ `me/contacts/${contactId}`,
493
+ null,
494
+ { $select: 'displayName,emailAddresses,companyName' }
495
+ );
496
+ const details = [
497
+ ...(contact.emailAddresses || []).map((e) => e.address).filter(Boolean),
498
+ contact.companyName,
499
+ ].filter(Boolean);
500
+ const name = contact.displayName || '(no name)';
501
+ return dryRunResult(
502
+ [
503
+ `Deletes contact '${name}'${details.length > 0 ? ` (${details.join('; ')})` : ''}.`,
504
+ "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), so treat it as permanent.",
505
+ ],
506
+ { action: 'delete', contactId, displayName: contact.displayName }
507
+ );
508
+ }
509
+
541
510
  /**
542
511
  * Delete contact handler
543
512
  */
@@ -545,12 +514,17 @@ async function handleDeleteContact(args) {
545
514
  const contactId = args.id;
546
515
 
547
516
  if (!contactId) {
548
- return { content: [{ type: 'text', text: 'Contact ID is required.' }] };
517
+ return toolError('Contact ID is required.');
549
518
  }
550
519
 
551
520
  try {
552
521
  const accessToken = await ensureAuthenticated();
553
522
 
523
+ // dryRun: say which contact would be lost; delete nothing.
524
+ if (args.dryRun) {
525
+ return await previewDeleteContact(accessToken, contactId);
526
+ }
527
+
554
528
  const endpoint = `me/contacts/${contactId}`;
555
529
  await callGraphAPI(accessToken, 'DELETE', endpoint);
556
530
 
@@ -565,20 +539,9 @@ async function handleDeleteContact(args) {
565
539
  };
566
540
  } catch (error) {
567
541
  if (error.message === 'Authentication required') {
568
- return {
569
- content: [
570
- {
571
- type: 'text',
572
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
573
- },
574
- ],
575
- };
542
+ return authRequiredError();
576
543
  }
577
- return {
578
- content: [
579
- { type: 'text', text: `Error deleting contact: ${error.message}` },
580
- ],
581
- };
544
+ return toolError(`Error deleting contact: ${error.message}`);
582
545
  }
583
546
  }
584
547
 
@@ -590,7 +553,7 @@ async function handleSearchPeople(args) {
590
553
  const count = Math.min(args.count || 25, 50);
591
554
 
592
555
  if (!query) {
593
- return { content: [{ type: 'text', text: 'Search query is required.' }] };
556
+ return toolError('Search query is required.');
594
557
  }
595
558
 
596
559
  try {
@@ -658,20 +621,9 @@ async function handleSearchPeople(args) {
658
621
  };
659
622
  } catch (error) {
660
623
  if (error.message === 'Authentication required') {
661
- return {
662
- content: [
663
- {
664
- type: 'text',
665
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
666
- },
667
- ],
668
- };
624
+ return authRequiredError();
669
625
  }
670
- return {
671
- content: [
672
- { type: 'text', text: `Error searching people: ${error.message}` },
673
- ],
674
- };
626
+ return toolError(`Error searching people: ${error.message}`);
675
627
  }
676
628
  }
677
629
 
@@ -680,13 +632,8 @@ const contactsTools = [
680
632
  {
681
633
  name: 'manage-contact',
682
634
  description:
683
- "Full CRUD over the signed-in user's personal Outlook contacts (destructive: covers `delete` action). action=`list` (default) returns contacts with pagination via `skip`/`count` (default 50). action=`search` returns contacts matching `query` against name/email (default 25). action=`get` returns full contact detail by `id`. action=`create` adds a new contact and returns its `id`. action=`update` patches the given fields by `id` (only fields passed are changed). action=`delete` permanently removes the contact by `id`. Use `outputVerbosity` (minimal/standard/full) on list/search to control field count. Prefer `search-people` for cross-source relevance ranking (contacts + directory + recent comms) — this tool only searches your personal contact store.",
684
- annotations: {
685
- title: 'Contacts',
686
- readOnlyHint: false,
687
- destructiveHint: true,
688
- openWorldHint: false,
689
- },
635
+ "Full CRUD over the signed-in user's personal Outlook contacts (destructive: covers `delete` action). action=`list` (default) returns contacts with pagination via `skip`/`count` (default 50). action=`search` returns contacts matching `query` against name/email (default 25). action=`get` returns full contact detail by `id`. action=`create` adds a new contact and returns its `id`. action=`update` patches the given fields by `id` (only fields passed are changed). action=`delete` removes the contact by `id`; it skips Deleted Items, so treat it as permanent and pass `dryRun: true` first to confirm which contact it is. Use `outputVerbosity` (minimal/standard/full) on list/search to control field count. Searches only your personal contact store; for relevance-ranked search across contacts, the directory and recent communications, use `search-people`.",
636
+ ...toolMetadata('manage-contact', 'Contacts'),
690
637
  inputSchema: {
691
638
  type: 'object',
692
639
  properties: {
@@ -768,12 +715,20 @@ const contactsTools = [
768
715
  type: 'string',
769
716
  description: 'Personal notes (action=create/update)',
770
717
  },
718
+ dryRun: {
719
+ type: 'boolean',
720
+ description:
721
+ 'Preview only (action=delete): nothing is deleted. Shows which contact would be removed. Other actions refuse dryRun and change nothing. Default false.',
722
+ },
771
723
  },
772
724
  additionalProperties: false,
773
725
  required: [],
774
726
  },
775
727
  handler: async (args) => {
776
728
  const action = args.action || 'list';
729
+ if (args.dryRun && action !== 'delete') {
730
+ return dryRunUnsupported('manage-contact', action, 'delete');
731
+ }
777
732
  switch (action) {
778
733
  case 'search':
779
734
  return handleSearchContacts(args);
@@ -788,28 +743,17 @@ const contactsTools = [
788
743
  case 'list':
789
744
  return handleListContacts(args);
790
745
  default:
791
- return {
792
- content: [
793
- {
794
- type: 'text',
795
- text: `Unknown action '${action}'. Valid actions: list, search, get, create, update, delete.`,
796
- },
797
- ],
798
- };
746
+ return toolError(
747
+ `Unknown action '${action}'. Valid actions: list, search, get, create, update, delete.`
748
+ );
799
749
  }
800
750
  },
801
751
  },
802
752
  {
803
753
  name: 'search-people',
804
754
  description:
805
- 'Relevance-ranked search across personal contacts, organisation directory, and recent communications via the Microsoft Graph People API (read-only). Returns people objects with `displayName`, `emailAddresses`, `companyName`, `jobTitle`, and relevance metadata — ideal for "who is X?" or "who do I email about Y?" lookups. Use `manage-contact` action=`search` instead when you specifically need entries from your personal contact store only.',
806
- annotations: {
807
- title: 'People Search',
808
- readOnlyHint: true,
809
- // openWorldHint: returns directory/people data for external contacts
810
- // (org directory + inferred from recent comms). (#92)
811
- openWorldHint: true,
812
- },
755
+ 'Relevance-ranked search across personal contacts, organisation directory, and recent communications via the Microsoft Graph People API (read-only). Returns people objects with `displayName`, `emailAddresses`, `companyName`, `jobTitle`, and relevance metadata, for "who is X?" or "who do I email about Y?" lookups. For entries from your personal contact store only, use `manage-contact` action=`search`.',
756
+ ...toolMetadata('search-people', 'People Search'),
813
757
  inputSchema: {
814
758
  type: 'object',
815
759
  properties: {
@@ -3,14 +3,20 @@
3
3
  * Provides tools to list and download email attachments via Microsoft Graph API
4
4
  */
5
5
  const _https = require('https'); // Reserved for future use
6
- const fs = require('fs');
7
6
  const os = require('os');
8
7
  const path = require('path');
9
8
  const _config = require('../config'); // Reserved for future use
10
9
  const { callGraphAPI } = require('../utils/graph-api');
11
10
  const { ensureAuthenticated } = require('../auth');
12
11
  const { buildMailboxPrefix } = require('../utils/mailbox');
13
- const { writeClaimedFile } = require('../utils/safe-write');
12
+ const {
13
+ writeClaimedFile,
14
+ ensureOutputDir,
15
+ confineOutputPath,
16
+ OutputPathError,
17
+ } = require('../utils/safe-write');
18
+ const { toolError, authRequiredError } = require('../utils/tool-error');
19
+ const { log } = require('../utils/logger');
14
20
 
15
21
  const MAX_FILENAME_LENGTH = 200;
16
22
 
@@ -52,14 +58,7 @@ async function handleListAttachments(args) {
52
58
  const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
53
59
 
54
60
  if (!messageId) {
55
- return {
56
- content: [
57
- {
58
- type: 'text',
59
- text: 'Error: messageId is required',
60
- },
61
- ],
62
- };
61
+ return toolError('Error: messageId is required');
63
62
  }
64
63
 
65
64
  try {
@@ -71,7 +70,7 @@ async function handleListAttachments(args) {
71
70
  $select: 'id,name,contentType,size,isInline',
72
71
  };
73
72
 
74
- console.error(`Fetching attachments for message: ${messageId}`);
73
+ log.debug(`Fetching attachments for message: ${messageId}`);
75
74
  const response = await callGraphAPI(
76
75
  accessToken,
77
76
  'GET',
@@ -113,24 +112,10 @@ async function handleListAttachments(args) {
113
112
  error.message === 'Authentication required' ||
114
113
  error.message === 'UNAUTHORIZED'
115
114
  ) {
116
- return {
117
- content: [
118
- {
119
- type: 'text',
120
- text: "Authentication required. Please use the 'authenticate' tool first.",
121
- },
122
- ],
123
- };
115
+ return authRequiredError();
124
116
  }
125
117
 
126
- return {
127
- content: [
128
- {
129
- type: 'text',
130
- text: `Error listing attachments: ${error.message}`,
131
- },
132
- ],
133
- };
118
+ return toolError(`Error listing attachments: ${error.message}`);
134
119
  }
135
120
  }
136
121
 
@@ -139,7 +124,7 @@ async function handleListAttachments(args) {
139
124
  * @param {object} args - Tool arguments
140
125
  * @param {string} args.messageId - The ID of the email message
141
126
  * @param {string} args.attachmentId - The ID of the attachment
142
- * @param {string} args.savePath - Optional path to save the file (defaults to current directory)
127
+ * @param {string} args.savePath - Deprecated alias for outputDir (absolute or ~/…; default: system temp directory)
143
128
  * @returns {object} - MCP response with download result
144
129
  */
145
130
  async function handleDownloadAttachment(args) {
@@ -152,14 +137,19 @@ async function handleDownloadAttachment(args) {
152
137
  const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
153
138
 
154
139
  if (!messageId || !attachmentId) {
155
- return {
156
- content: [
157
- {
158
- type: 'text',
159
- text: 'Error: Both messageId and attachmentId are required',
160
- },
161
- ],
162
- };
140
+ return toolError('Error: Both messageId and attachmentId are required');
141
+ }
142
+
143
+ // Determine save location before fetching anything. F-19: default to
144
+ // os.tmpdir() instead of cwd so attachments don't silently land in the
145
+ // source tree when the caller forgets to pass outputDir. The directory
146
+ // must be inside an allowed base; write only to the resolved path.
147
+ let outputDir;
148
+ try {
149
+ outputDir = confineOutputPath(savePath || os.tmpdir());
150
+ } catch (error) {
151
+ if (!(error instanceof OutputPathError)) throw error;
152
+ return toolError(error.message, { nextStep: error.nextStep });
163
153
  }
164
154
 
165
155
  try {
@@ -167,7 +157,7 @@ async function handleDownloadAttachment(args) {
167
157
 
168
158
  // First, get attachment metadata to get the filename and content
169
159
  const metadataEndpoint = `${prefix}/messages/${messageId}/attachments/${attachmentId}`;
170
- console.error(`Fetching attachment metadata: ${attachmentId}`);
160
+ log.debug(`Fetching attachment metadata: ${attachmentId}`);
171
161
 
172
162
  const metadata = await callGraphAPI(
173
163
  accessToken,
@@ -178,14 +168,7 @@ async function handleDownloadAttachment(args) {
178
168
  );
179
169
 
180
170
  if (!metadata) {
181
- return {
182
- content: [
183
- {
184
- type: 'text',
185
- text: 'Error: Attachment not found',
186
- },
187
- ],
188
- };
171
+ return toolError('Error: Attachment not found');
189
172
  }
190
173
 
191
174
  const filename = metadata.name || 'attachment';
@@ -196,24 +179,13 @@ async function handleDownloadAttachment(args) {
196
179
  const contentBytes = metadata.contentBytes;
197
180
 
198
181
  if (!contentBytes) {
199
- return {
200
- content: [
201
- {
202
- type: 'text',
203
- text: 'Error: No content found in attachment',
204
- },
205
- ],
206
- };
182
+ return toolError('Error: No content found in attachment');
207
183
  }
208
184
 
209
- // Determine save location. F-19: default to os.tmpdir() instead
210
- // of cwd so attachments don't silently land in the source tree
211
- // when the caller forgets to pass outputDir. Auto-create the
212
- // target directory.
185
+ // Auto-create the target directory.
213
186
  // The filename is sender-controlled (GHSA-755c-c45g-69rv): reduce it
214
187
  // to a safe basename and never overwrite or follow a symlink.
215
- const outputDir = savePath || os.tmpdir();
216
- fs.mkdirSync(outputDir, { recursive: true });
188
+ ensureOutputDir(outputDir);
217
189
 
218
190
  // Decode base64 and save to file
219
191
  const buffer = Buffer.from(contentBytes, 'base64');
@@ -239,14 +211,9 @@ async function handleDownloadAttachment(args) {
239
211
  };
240
212
  } else if (metadata['@odata.type'] === '#microsoft.graph.itemAttachment') {
241
213
  // Item attachments (embedded emails, calendar items) need different handling
242
- return {
243
- content: [
244
- {
245
- type: 'text',
246
- text: `This is an embedded item attachment (${metadata.name}). Item attachments cannot be downloaded as files directly. They contain embedded Outlook items like emails or calendar events.`,
247
- },
248
- ],
249
- };
214
+ return toolError(
215
+ `This is an embedded item attachment (${metadata.name}). Item attachments cannot be downloaded as files directly. They contain embedded Outlook items like emails or calendar events.`
216
+ );
250
217
  } else if (
251
218
  metadata['@odata.type'] === '#microsoft.graph.referenceAttachment'
252
219
  ) {
@@ -260,38 +227,17 @@ async function handleDownloadAttachment(args) {
260
227
  ],
261
228
  };
262
229
  } else {
263
- return {
264
- content: [
265
- {
266
- type: 'text',
267
- text: `Unknown attachment type: ${metadata['@odata.type']}`,
268
- },
269
- ],
270
- };
230
+ return toolError(`Unknown attachment type: ${metadata['@odata.type']}`);
271
231
  }
272
232
  } catch (error) {
273
233
  if (
274
234
  error.message === 'Authentication required' ||
275
235
  error.message === 'UNAUTHORIZED'
276
236
  ) {
277
- return {
278
- content: [
279
- {
280
- type: 'text',
281
- text: "Authentication required. Please use the 'authenticate' tool first.",
282
- },
283
- ],
284
- };
237
+ return authRequiredError();
285
238
  }
286
239
 
287
- return {
288
- content: [
289
- {
290
- type: 'text',
291
- text: `Error downloading attachment: ${error.message}`,
292
- },
293
- ],
294
- };
240
+ return toolError(`Error downloading attachment: ${error.message}`);
295
241
  }
296
242
  }
297
243
 
@@ -307,33 +253,19 @@ async function handleGetAttachmentContent(args) {
307
253
  const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
308
254
 
309
255
  if (!messageId || !attachmentId) {
310
- return {
311
- content: [
312
- {
313
- type: 'text',
314
- text: 'Error: Both messageId and attachmentId are required',
315
- },
316
- ],
317
- };
256
+ return toolError('Error: Both messageId and attachmentId are required');
318
257
  }
319
258
 
320
259
  try {
321
260
  const accessToken = await ensureAuthenticated();
322
261
 
323
262
  const endpoint = `${prefix}/messages/${messageId}/attachments/${attachmentId}`;
324
- console.error(`Fetching attachment content: ${attachmentId}`);
263
+ log.debug(`Fetching attachment content: ${attachmentId}`);
325
264
 
326
265
  const response = await callGraphAPI(accessToken, 'GET', endpoint, null, {});
327
266
 
328
267
  if (!response) {
329
- return {
330
- content: [
331
- {
332
- type: 'text',
333
- text: 'Error: Attachment not found',
334
- },
335
- ],
336
- };
268
+ return toolError('Error: Attachment not found');
337
269
  }
338
270
 
339
271
  const filename = response.name || 'attachment';
@@ -371,7 +303,7 @@ async function handleGetAttachmentContent(args) {
371
303
  content: [
372
304
  {
373
305
  type: 'text',
374
- text: `Attachment: ${filename}\nType: ${contentType}\nSize: ${sizeKB} KB\n\nThis is a binary file. Use 'download-attachment' to save it to disk.`,
306
+ text: `Attachment: ${filename}\nType: ${contentType}\nSize: ${sizeKB} KB\n\nThis is a binary file. Use \`attachments\` action=\`download\` to save it to disk.`,
375
307
  },
376
308
  ],
377
309
  };
@@ -390,24 +322,10 @@ async function handleGetAttachmentContent(args) {
390
322
  error.message === 'Authentication required' ||
391
323
  error.message === 'UNAUTHORIZED'
392
324
  ) {
393
- return {
394
- content: [
395
- {
396
- type: 'text',
397
- text: "Authentication required. Please use the 'authenticate' tool first.",
398
- },
399
- ],
400
- };
325
+ return authRequiredError();
401
326
  }
402
327
 
403
- return {
404
- content: [
405
- {
406
- type: 'text',
407
- text: `Error getting attachment content: ${error.message}`,
408
- },
409
- ],
410
- };
328
+ return toolError(`Error getting attachment content: ${error.message}`);
411
329
  }
412
330
  }
413
331