@littlebearapps/outlook-assistant 3.3.1 → 3.4.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.
package/README.md CHANGED
@@ -2,14 +2,20 @@
2
2
  <img src="docs/assets/outlook-assistant-logo-full.svg" height="200" alt="Outlook Assistant" />
3
3
  </p>
4
4
 
5
+ <h1 align="center">Outlook Assistant</h1>
6
+
5
7
  <p align="center">
6
- <strong>Let your AI assistant read, search, send, and manage your Outlook email, calendar, and contacts — all from the conversation.</strong>
8
+ <strong>MCP server for Outlook email, calendar, and contacts — let your AI assistant manage your inbox directly from the conversation.</strong>
7
9
  </p>
8
10
 
9
11
  <p align="center">
10
12
  <a href="https://www.npmjs.com/package/@littlebearapps/outlook-assistant"><img src="https://img.shields.io/npm/v/@littlebearapps/outlook-assistant" alt="npm version" /></a>
11
13
  <a href="https://www.npmjs.com/package/@littlebearapps/outlook-assistant"><img src="https://img.shields.io/npm/dm/@littlebearapps/outlook-assistant" alt="npm downloads" /></a>
14
+ <a href="https://github.com/littlebearapps/outlook-assistant/stargazers"><img src="https://img.shields.io/github/stars/littlebearapps/outlook-assistant" alt="GitHub stars" /></a>
15
+ <a href="https://github.com/littlebearapps/outlook-assistant/commits/main"><img src="https://img.shields.io/github/last-commit/littlebearapps/outlook-assistant" alt="Last commit" /></a>
12
16
  <a href="https://github.com/littlebearapps/outlook-assistant/actions/workflows/ci.yml"><img src="https://github.com/littlebearapps/outlook-assistant/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
17
+ <a href="https://github.com/littlebearapps/outlook-assistant/actions/workflows/codeql.yml"><img src="https://github.com/littlebearapps/outlook-assistant/actions/workflows/codeql.yml/badge.svg" alt="CodeQL" /></a>
18
+ <a href="https://github.com/littlebearapps/outlook-assistant/issues"><img src="https://img.shields.io/github/issues/littlebearapps/outlook-assistant" alt="Open issues" /></a>
13
19
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
14
20
  <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen" alt="Node.js" /></a>
15
21
  </p>
@@ -79,6 +85,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
79
85
  | `markdown` | `.md` | Pasting into documents, feeding into AI workflows |
80
86
  | `json` | `.json` | Data analysis, pipeline processing, compliance reporting |
81
87
  | `html` | `.html` | Visual archival with formatting intact |
88
+ | `csv` | `.csv` | Spreadsheet import, bulk metadata analysis, compliance audits |
82
89
 
83
90
  Export individual emails, search results, or entire conversation threads — use `target=messages` with a search query to batch-export without manually collecting IDs.
84
91
 
@@ -150,7 +157,58 @@ You need a Microsoft Azure app registration to authenticate. See the **[Azure Se
150
157
 
151
158
  ### 3. Configure Your MCP Client
152
159
 
153
- Add to your MCP client config. For Claude Desktop (`claude_desktop_config.json`):
160
+ Add to your MCP client config:
161
+
162
+ <details>
163
+ <summary><strong>Claude Desktop</strong> (<code>claude_desktop_config.json</code>)</summary>
164
+
165
+ ```json
166
+ {
167
+ "mcpServers": {
168
+ "outlook": {
169
+ "command": "npx",
170
+ "args": ["@littlebearapps/outlook-assistant"],
171
+ "env": {
172
+ "OUTLOOK_CLIENT_ID": "your-application-client-id",
173
+ "OUTLOOK_CLIENT_SECRET": "your-client-secret-VALUE"
174
+ }
175
+ }
176
+ }
177
+ }
178
+ ```
179
+ </details>
180
+
181
+ <details>
182
+ <summary><strong>Claude Code</strong> (CLI)</summary>
183
+
184
+ ```bash
185
+ claude mcp add outlook -- npx @littlebearapps/outlook-assistant
186
+ ```
187
+
188
+ Then set environment variables in your `.env` or shell.
189
+ </details>
190
+
191
+ <details>
192
+ <summary><strong>Cursor</strong> (<code>.cursor/mcp.json</code>)</summary>
193
+
194
+ ```json
195
+ {
196
+ "mcpServers": {
197
+ "outlook": {
198
+ "command": "npx",
199
+ "args": ["@littlebearapps/outlook-assistant"],
200
+ "env": {
201
+ "OUTLOOK_CLIENT_ID": "your-application-client-id",
202
+ "OUTLOOK_CLIENT_SECRET": "your-client-secret-VALUE"
203
+ }
204
+ }
205
+ }
206
+ }
207
+ ```
208
+ </details>
209
+
210
+ <details>
211
+ <summary><strong>Windsurf</strong> (<code>~/.codeium/windsurf/mcp_config.json</code>)</summary>
154
212
 
155
213
  ```json
156
214
  {
@@ -166,6 +224,7 @@ Add to your MCP client config. For Claude Desktop (`claude_desktop_config.json`)
166
224
  }
167
225
  }
168
226
  ```
227
+ </details>
169
228
 
170
229
  ### 4. Authenticate
171
230
 
@@ -258,24 +317,9 @@ USE_TEST_MODE=false
258
317
 
259
318
  ### MCP Client Configuration
260
319
 
261
- Add to your MCP client config (example for Claude Desktop):
262
-
263
- ```json
264
- {
265
- "mcpServers": {
266
- "outlook": {
267
- "command": "npx",
268
- "args": ["@littlebearapps/outlook-assistant"],
269
- "env": {
270
- "OUTLOOK_CLIENT_ID": "your-application-client-id",
271
- "OUTLOOK_CLIENT_SECRET": "your-client-secret-VALUE"
272
- }
273
- }
274
- }
275
- }
276
- ```
320
+ See [Quick Start — Configure Your MCP Client](#3-configure-your-mcp-client) above for Claude Desktop, Claude Code, Cursor, and Windsurf configs.
277
321
 
278
- Or if installed from source:
322
+ If installed from source, use `node` instead of `npx`:
279
323
 
280
324
  ```json
281
325
  {
@@ -427,6 +471,10 @@ For security concerns, please see our [Security Policy](SECURITY.md). Do not ope
427
471
 
428
472
  See [CHANGELOG.md](CHANGELOG.md) for version history.
429
473
 
474
+ ## Listed On
475
+
476
+ <a href="https://glama.ai/mcp/servers/littlebearapps/outlook-assistant"><img width="190" height="100" src="https://glama.ai/mcp/servers/littlebearapps/outlook-assistant/badge" alt="Outlook Assistant on Glama" /></a>
477
+
430
478
  ## About
431
479
 
432
480
  Built and maintained by [Little Bear Apps](https://littlebearapps.com). Outlook Assistant is open source under the [MIT License](LICENSE).
package/advanced/index.js CHANGED
@@ -11,6 +11,7 @@
11
11
  const { callGraphAPI } = require('../utils/graph-api');
12
12
  const { ensureAuthenticated } = require('../auth');
13
13
  const { FIELD_PRESETS } = require('../utils/field-presets');
14
+ const { DEFAULT_TIMEZONE } = require('../config');
14
15
 
15
16
  /**
16
17
  * Format an email for display (simplified)
@@ -224,16 +225,32 @@ async function handleSetMessageFlag(args) {
224
225
  };
225
226
 
226
227
  if (dueDateTime) {
228
+ // Graph API expects { dateTime, timeZone } envelope without trailing Z
229
+ // When timeZone is specified, the dateTime value is interpreted in that zone
230
+ const dueDt = dueDateTime.replace(/Z$/i, '');
227
231
  flag.dueDateTime = {
228
- dateTime: new Date(dueDateTime).toISOString(),
229
- timeZone: 'UTC',
232
+ dateTime: dueDt,
233
+ timeZone: DEFAULT_TIMEZONE,
230
234
  };
231
- }
232
235
 
233
- if (startDateTime) {
236
+ // Graph API requires startDateTime when dueDateTime is set
237
+ // Default to start of the same day if not explicitly provided
238
+ if (startDateTime) {
239
+ flag.startDateTime = {
240
+ dateTime: startDateTime.replace(/Z$/i, ''),
241
+ timeZone: DEFAULT_TIMEZONE,
242
+ };
243
+ } else {
244
+ const startOfDay = dueDt.split('T')[0] + 'T09:00:00';
245
+ flag.startDateTime = {
246
+ dateTime: startOfDay,
247
+ timeZone: DEFAULT_TIMEZONE,
248
+ };
249
+ }
250
+ } else if (startDateTime) {
234
251
  flag.startDateTime = {
235
- dateTime: new Date(startDateTime).toISOString(),
236
- timeZone: 'UTC',
252
+ dateTime: startDateTime.replace(/Z$/i, ''),
253
+ timeZone: DEFAULT_TIMEZONE,
237
254
  };
238
255
  }
239
256
 
package/config.js CHANGED
@@ -26,7 +26,7 @@ if (!homeDir) {
26
26
  module.exports = {
27
27
  // Server information
28
28
  SERVER_NAME: 'outlook-assistant',
29
- SERVER_VERSION: '3.3.0',
29
+ SERVER_VERSION: require('./package.json').version,
30
30
 
31
31
  // Test mode setting
32
32
  USE_TEST_MODE: process.env.USE_TEST_MODE === 'true',
@@ -14,6 +14,7 @@ const { ensureAuthenticated } = require('../auth');
14
14
  const { getEmailFields } = require('../utils/field-presets');
15
15
  const {
16
16
  formatEmailContent,
17
+ formatEmailsAsCSV,
17
18
  VERBOSITY,
18
19
  } = require('../utils/response-formatter');
19
20
 
@@ -329,7 +330,7 @@ async function handleExportConversation(args) {
329
330
  };
330
331
  }
331
332
 
332
- const validFormats = ['eml', 'mbox', 'markdown', 'json', 'html'];
333
+ const validFormats = ['eml', 'mbox', 'markdown', 'json', 'html', 'csv'];
333
334
  if (!validFormats.includes(format)) {
334
335
  return {
335
336
  content: [
@@ -602,6 +603,16 @@ async function handleExportConversation(args) {
602
603
  exportedFiles.push(htmlPath);
603
604
  break;
604
605
  }
606
+
607
+ case 'csv': {
608
+ // Export as CSV
609
+ const csvPath = path.join(resolvedDir, `${filenameBase}.csv`);
610
+ const csvContent = formatEmailsAsCSV(messages);
611
+ fs.writeFileSync(csvPath, csvContent, 'utf8');
612
+ exportStats.bytes = Buffer.byteLength(csvContent, 'utf8');
613
+ exportedFiles.push(csvPath);
614
+ break;
615
+ }
605
616
  }
606
617
 
607
618
  // Format result
package/email/export.js CHANGED
@@ -11,6 +11,7 @@ const { callGraphAPI, callGraphAPIRaw } = require('../utils/graph-api');
11
11
  const { ensureAuthenticated } = require('../auth');
12
12
  const {
13
13
  formatEmailContent,
14
+ formatEmailsAsCSV,
14
15
  VERBOSITY,
15
16
  } = require('../utils/response-formatter');
16
17
  const { getEmailFields } = require('../utils/field-presets');
@@ -21,6 +22,7 @@ const EXPORT_FORMATS = {
21
22
  EML: 'eml', // Alias for MIME
22
23
  MARKDOWN: 'markdown',
23
24
  JSON: 'json',
25
+ CSV: 'csv',
24
26
  };
25
27
 
26
28
  /**
@@ -111,12 +113,15 @@ async function handleExportEmail(args) {
111
113
  } else if (format === EXPORT_FORMATS.JSON) {
112
114
  // JSON export - full email object
113
115
  content = JSON.stringify(email, null, 2);
116
+ } else if (format === EXPORT_FORMATS.CSV) {
117
+ // CSV export - email metadata
118
+ content = formatEmailsAsCSV(email);
114
119
  } else {
115
120
  return {
116
121
  content: [
117
122
  {
118
123
  type: 'text',
119
- text: `Unknown format: ${format}. Supported: mime, eml, markdown, json`,
124
+ text: `Unknown format: ${format}. Supported: ${Object.values(EXPORT_FORMATS).join(', ')}`,
120
125
  },
121
126
  ],
122
127
  };
@@ -254,6 +259,68 @@ async function handleBatchExportEmails(args) {
254
259
  console.error(`Batch export limited to ${maxBatch} emails`);
255
260
  }
256
261
 
262
+ // CSV batch export: aggregate all emails into a single CSV file
263
+ if (format === EXPORT_FORMATS.CSV) {
264
+ const selectFields = getEmailFields('export');
265
+ const emails = [];
266
+ const failed = [];
267
+
268
+ for (const emailId of idsToExport) {
269
+ try {
270
+ const email = await callGraphAPI(
271
+ accessToken,
272
+ 'GET',
273
+ `me/messages/${emailId}`,
274
+ null,
275
+ { $select: selectFields }
276
+ );
277
+ emails.push(email);
278
+ } catch (error) {
279
+ failed.push({ emailId, error: error.message });
280
+ }
281
+ }
282
+
283
+ const csvContent = formatEmailsAsCSV(emails);
284
+ const csvPath = path.join(
285
+ outputDir,
286
+ `batch_export_${new Date().toISOString().slice(0, 10)}.csv`
287
+ );
288
+ fs.writeFileSync(csvPath, csvContent, 'utf8');
289
+ const totalBytes = Buffer.byteLength(csvContent, 'utf8');
290
+
291
+ let resultText = `## Batch Export Complete\n\n`;
292
+ resultText += `| Metric | Value |\n`;
293
+ resultText += `|--------|-------|\n`;
294
+ resultText += `| Total | ${idsToExport.length} |\n`;
295
+ resultText += `| Successful | ${emails.length} |\n`;
296
+ resultText += `| Failed | ${failed.length} |\n`;
297
+ resultText += `| Output File | \`${csvPath}\` |\n`;
298
+ resultText += `| Format | CSV |\n`;
299
+ resultText += `| Total Size | ${(totalBytes / 1024).toFixed(1)} KB |\n`;
300
+
301
+ if (failed.length > 0) {
302
+ resultText += `\n### Failed Exports\n\n`;
303
+ for (const f of failed.slice(0, 10)) {
304
+ resultText += `- ID \`${f.emailId}\`: ${f.error}\n`;
305
+ }
306
+ if (failed.length > 10) {
307
+ resultText += `- ... and ${failed.length - 10} more\n`;
308
+ }
309
+ }
310
+
311
+ return {
312
+ content: [{ type: 'text', text: resultText }],
313
+ _meta: {
314
+ outputDir,
315
+ format,
316
+ total: idsToExport.length,
317
+ successful: emails.length,
318
+ failed: failed.length,
319
+ totalBytes,
320
+ },
321
+ };
322
+ }
323
+
257
324
  // Export emails with concurrency limit (4 concurrent per Graph API limits)
258
325
  const results = await exportWithConcurrency(
259
326
  accessToken,
@@ -559,6 +626,8 @@ function getExtension(format) {
559
626
  return 'eml';
560
627
  case EXPORT_FORMATS.JSON:
561
628
  return 'json';
629
+ case EXPORT_FORMATS.CSV:
630
+ return 'csv';
562
631
  case EXPORT_FORMATS.MARKDOWN:
563
632
  default:
564
633
  return 'md';
@@ -10,15 +10,39 @@ const { callGraphAPI } = require('../utils/graph-api');
10
10
  const _folderCache = {};
11
11
 
12
12
  /**
13
- * Well-known folder names and their endpoints
13
+ * Well-known folder names and their endpoints.
14
+ * Includes Graph API well-known names (sentitems, deleteditems, junkemail),
15
+ * common display names (Sent Items, Deleted Items, Junk Email), and
16
+ * short aliases (sent, deleted, junk) for consistent resolution across tools.
14
17
  */
15
18
  const WELL_KNOWN_FOLDERS = {
19
+ // Inbox
16
20
  inbox: 'me/mailFolders/inbox/messages',
21
+
22
+ // Drafts
17
23
  drafts: 'me/mailFolders/drafts/messages',
24
+
25
+ // Sent Items - alias, Graph API name, display name
18
26
  sent: 'me/mailFolders/sentItems/messages',
27
+ sentitems: 'me/mailFolders/sentItems/messages',
28
+ 'sent items': 'me/mailFolders/sentItems/messages',
29
+
30
+ // Deleted Items - alias, Graph API name, display name
19
31
  deleted: 'me/mailFolders/deletedItems/messages',
32
+ deleteditems: 'me/mailFolders/deletedItems/messages',
33
+ 'deleted items': 'me/mailFolders/deletedItems/messages',
34
+
35
+ // Junk Email - alias, Graph API name, display name, common alias
20
36
  junk: 'me/mailFolders/junkemail/messages',
37
+ junkemail: 'me/mailFolders/junkemail/messages',
38
+ 'junk email': 'me/mailFolders/junkemail/messages',
39
+ spam: 'me/mailFolders/junkemail/messages',
40
+
41
+ // Archive
21
42
  archive: 'me/mailFolders/archive/messages',
43
+
44
+ // Outbox
45
+ outbox: 'me/mailFolders/outbox/messages',
22
46
  };
23
47
 
24
48
  /**
package/email/index.js CHANGED
@@ -434,9 +434,9 @@ const emailTools = [
434
434
  },
435
435
  format: {
436
436
  type: 'string',
437
- enum: ['mime', 'eml', 'markdown', 'json', 'mbox', 'html'],
437
+ enum: ['mime', 'eml', 'markdown', 'json', 'mbox', 'html', 'csv'],
438
438
  description:
439
- 'Export format (target=message: mime/eml/markdown/json, target=conversation: eml/mbox/markdown/json/html)',
439
+ 'Export format (target=message: mime/eml/markdown/json/csv, target=conversation: eml/mbox/markdown/json/html/csv)',
440
440
  },
441
441
  savePath: {
442
442
  type: 'string',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@littlebearapps/outlook-assistant",
3
- "version": "3.3.1",
3
+ "version": "3.4.1",
4
4
  "mcpName": "io.github.littlebearapps/outlook-assistant",
5
5
  "description": "Outlook Assistant — MCP server with 20 tools for email, calendar, contacts, and settings via Microsoft Graph API",
6
6
  "main": "index.js",
@@ -17,7 +17,8 @@
17
17
  "lint:fix": "eslint . --fix",
18
18
  "format": "prettier --write .",
19
19
  "format:check": "prettier --check .",
20
- "prepare": "husky"
20
+ "prepare": "husky || true",
21
+ "version": "node -e \"const s=require('./server.json');const p=require('./package.json');s.version=p.version;s.packages[0].version=p.version;require('fs').writeFileSync('./server.json',JSON.stringify(s,null,2)+'\\n')\" && git add server.json"
21
22
  },
22
23
  "lint-staged": {
23
24
  "*.js": [
package/settings/index.js CHANGED
@@ -129,7 +129,9 @@ async function handleGetMailboxSettings(args) {
129
129
  output.push(`**Locale**: ${settings.locale || 'Not set'}`);
130
130
  output.push(`**Display Name**: ${settings.displayName || 'Not set'}`);
131
131
  } else if (section === 'timeZone') {
132
- output.push(`**Zone**: ${settings}`);
132
+ // Graph API returns { value: "timezone string" } for scalar properties
133
+ const tz = typeof settings === 'string' ? settings : settings.value;
134
+ output.push(`**Zone**: ${tz || 'Not set'}`);
133
135
  } else {
134
136
  // Fallback for unknown sections
135
137
  output.push('```json');
@@ -283,6 +283,49 @@ function formatEmailContent(
283
283
  return output;
284
284
  }
285
285
 
286
+ /**
287
+ * Formats one or more emails as a CSV string with a header row.
288
+ * Accepts a single email object or an array of email objects.
289
+ * Only exports metadata columns - no body content.
290
+ * @param {object|object[]} emails - Single email or array of emails from Graph API
291
+ * @returns {string} - CSV string with header row and one row per email
292
+ */
293
+ function formatEmailsAsCSV(emails) {
294
+ const CSV_HEADERS = [
295
+ 'id',
296
+ 'subject',
297
+ 'from',
298
+ 'to',
299
+ 'cc',
300
+ 'receivedDateTime',
301
+ 'isRead',
302
+ 'importance',
303
+ 'hasAttachments',
304
+ ];
305
+
306
+ const emailList = Array.isArray(emails) ? emails : [emails];
307
+
308
+ const rows = emailList.map((email) => {
309
+ const from = formatEmailAddress(email.from?.emailAddress);
310
+ const to = formatRecipients(email.toRecipients);
311
+ const cc = formatRecipients(email.ccRecipients);
312
+
313
+ return [
314
+ email.id || '',
315
+ email.subject || '',
316
+ from,
317
+ to,
318
+ cc,
319
+ email.receivedDateTime || '',
320
+ email.isRead != null ? String(email.isRead) : '',
321
+ email.importance || '',
322
+ email.hasAttachments != null ? String(email.hasAttachments) : '',
323
+ ].map(escapeCSV);
324
+ });
325
+
326
+ return [CSV_HEADERS.join(','), ...rows.map((r) => r.join(','))].join('\n');
327
+ }
328
+
286
329
  /**
287
330
  * Formats email headers for legal/forensic use
288
331
  * @param {Array} headers - Array of header objects
@@ -435,6 +478,26 @@ function stripHtml(html) {
435
478
  .trim();
436
479
  }
437
480
 
481
+ function escapeCSV(value) {
482
+ if (value === null || value === undefined) return '';
483
+ const str = String(value);
484
+ const needsQuoting =
485
+ str.includes(',') ||
486
+ str.includes('"') ||
487
+ str.includes('\n') ||
488
+ str.includes('\r');
489
+ const formulaChars = ['=', '+', '-', '@', '\t', '\r', '\n'];
490
+ const isFormula = formulaChars.some((ch) => str.startsWith(ch));
491
+
492
+ if (isFormula) {
493
+ return '"' + "'" + str.replace(/"/g, '""') + '"';
494
+ }
495
+ if (needsQuoting) {
496
+ return '"' + str.replace(/"/g, '""') + '"';
497
+ }
498
+ return str;
499
+ }
500
+
438
501
  module.exports = {
439
502
  VERBOSITY,
440
503
  DEFAULT_LIMITS,
@@ -445,6 +508,7 @@ module.exports = {
445
508
  formatEmailList,
446
509
  formatEmailListAsTable,
447
510
  formatEmailContent,
511
+ formatEmailsAsCSV,
448
512
  formatEmailHeaders,
449
513
  createResponseMeta,
450
514
  wrapMcpResponse,
@@ -454,4 +518,5 @@ module.exports = {
454
518
  formatRecipients,
455
519
  truncateText,
456
520
  stripHtml,
521
+ escapeCSV,
457
522
  };