@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 +67 -19
- package/email/conversations.js +12 -1
- package/email/export.js +70 -1
- package/email/index.js +2 -2
- package/package.json +2 -2
- package/utils/response-formatter.js +65 -0
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>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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/email/conversations.js
CHANGED
|
@@ -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:
|
|
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
|
+
"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
|
};
|