@littlebearapps/outlook-assistant 3.3.1 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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).
@@ -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';
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.0",
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,7 @@
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
21
  },
22
22
  "lint-staged": {
23
23
  "*.js": [
@@ -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
  };