@littlebearapps/outlook-assistant 3.7.1 → 3.7.4
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 +35 -18
- package/advanced/index.js +26 -3
- package/auth/token-storage.js +18 -6
- package/auth/tools.js +121 -4
- package/calendar/index.js +27 -1
- package/categories/index.js +67 -28
- package/contacts/index.js +118 -27
- package/email/attachments.js +13 -3
- package/email/conversations.js +4 -7
- package/email/delta.js +20 -6
- package/email/export.js +26 -3
- package/email/index.js +38 -4
- package/email/list.js +6 -1
- package/email/mail-tips.js +26 -2
- package/email/search.js +82 -25
- package/folder/create.js +6 -1
- package/folder/index.js +10 -1
- package/index.js +33 -0
- package/llms.txt +8 -2
- package/package.json +1 -1
- package/rules/create.js +5 -1
- package/rules/index.js +19 -1
- package/rules/update.js +11 -3
- package/settings/index.js +70 -2
- package/utils/response-formatter.js +27 -0
- package/utils/schema-coerce.js +248 -0
package/email/index.js
CHANGED
|
@@ -137,6 +137,7 @@ const emailTools = [
|
|
|
137
137
|
'Include email headers for each message (conversationId only)',
|
|
138
138
|
},
|
|
139
139
|
},
|
|
140
|
+
additionalProperties: false,
|
|
140
141
|
required: [],
|
|
141
142
|
},
|
|
142
143
|
handler: async (args) => {
|
|
@@ -223,6 +224,7 @@ const emailTools = [
|
|
|
223
224
|
'Return raw JSON instead of Markdown (headersMode only, default: false)',
|
|
224
225
|
},
|
|
225
226
|
},
|
|
227
|
+
additionalProperties: false,
|
|
226
228
|
required: ['id'],
|
|
227
229
|
},
|
|
228
230
|
handler: async (args) => {
|
|
@@ -286,6 +288,7 @@ const emailTools = [
|
|
|
286
288
|
'Check recipients for out-of-office, mailbox full, delivery restrictions before sending (default: false). Combine with dryRun=true for pre-send review.',
|
|
287
289
|
},
|
|
288
290
|
},
|
|
291
|
+
additionalProperties: false,
|
|
289
292
|
required: ['to', 'subject', 'body'],
|
|
290
293
|
},
|
|
291
294
|
handler: handleSendEmail,
|
|
@@ -364,6 +367,7 @@ const emailTools = [
|
|
|
364
367
|
'Check recipients for out-of-office, delivery restrictions before saving (action=create, default: false)',
|
|
365
368
|
},
|
|
366
369
|
},
|
|
370
|
+
additionalProperties: false,
|
|
367
371
|
required: ['action'],
|
|
368
372
|
},
|
|
369
373
|
handler: handleDraft,
|
|
@@ -408,6 +412,7 @@ const emailTools = [
|
|
|
408
412
|
description: 'Start date/time for follow-up, ISO 8601 (action=flag)',
|
|
409
413
|
},
|
|
410
414
|
},
|
|
415
|
+
additionalProperties: false,
|
|
411
416
|
required: ['action'],
|
|
412
417
|
},
|
|
413
418
|
handler: async (args) => {
|
|
@@ -473,12 +478,18 @@ const emailTools = [
|
|
|
473
478
|
type: 'string',
|
|
474
479
|
description: 'Attachment ID (action=view/download, required)',
|
|
475
480
|
},
|
|
481
|
+
outputDir: {
|
|
482
|
+
type: 'string',
|
|
483
|
+
description:
|
|
484
|
+
'Directory to save file (action=download, default: system tmpdir). Auto-created if missing.',
|
|
485
|
+
},
|
|
476
486
|
savePath: {
|
|
477
487
|
type: 'string',
|
|
478
488
|
description:
|
|
479
|
-
'
|
|
489
|
+
'DEPRECATED alias for `outputDir`. Will be removed in v3.8.0.',
|
|
480
490
|
},
|
|
481
491
|
},
|
|
492
|
+
additionalProperties: false,
|
|
482
493
|
required: ['messageId'],
|
|
483
494
|
},
|
|
484
495
|
handler: async (args) => {
|
|
@@ -489,8 +500,16 @@ const emailTools = [
|
|
|
489
500
|
case 'download':
|
|
490
501
|
return handleDownloadAttachment(args);
|
|
491
502
|
case 'list':
|
|
492
|
-
default:
|
|
493
503
|
return handleListAttachments(args);
|
|
504
|
+
default:
|
|
505
|
+
return {
|
|
506
|
+
content: [
|
|
507
|
+
{
|
|
508
|
+
type: 'text',
|
|
509
|
+
text: `Unknown action '${action}'. Valid actions: list, view, download.`,
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
};
|
|
494
513
|
}
|
|
495
514
|
},
|
|
496
515
|
},
|
|
@@ -521,7 +540,7 @@ const emailTools = [
|
|
|
521
540
|
type: 'string',
|
|
522
541
|
enum: ['mime', 'eml', 'markdown', 'json', 'mbox', 'html', 'csv'],
|
|
523
542
|
description:
|
|
524
|
-
'Export format
|
|
543
|
+
'Export format. Valid values vary by target: target=message accepts mime/eml/markdown/json/csv (mbox and html are conversation-only). target=conversation accepts eml/mbox/markdown/json/html/csv. target=messages (batch) accepts markdown/json/csv. mime is an alias for eml (same RFC822 bytes, .eml extension on disk).',
|
|
525
544
|
},
|
|
526
545
|
savePath: {
|
|
527
546
|
type: 'string',
|
|
@@ -551,6 +570,11 @@ const emailTools = [
|
|
|
551
570
|
description:
|
|
552
571
|
'Search query to find emails (target=messages, alternative to emailIds)',
|
|
553
572
|
},
|
|
573
|
+
query: {
|
|
574
|
+
type: 'string',
|
|
575
|
+
description:
|
|
576
|
+
'Free-text search shortcut (target=messages). Equivalent to passing searchQuery: { subject: <query> }. Convenience alias for callers used to search-emails.',
|
|
577
|
+
},
|
|
554
578
|
outputDir: {
|
|
555
579
|
type: 'string',
|
|
556
580
|
description:
|
|
@@ -581,6 +605,7 @@ const emailTools = [
|
|
|
581
605
|
description: 'Max content size in bytes (target=mime, default: 1MB)',
|
|
582
606
|
},
|
|
583
607
|
},
|
|
608
|
+
additionalProperties: false,
|
|
584
609
|
required: [],
|
|
585
610
|
},
|
|
586
611
|
handler: async (args) => {
|
|
@@ -593,8 +618,16 @@ const emailTools = [
|
|
|
593
618
|
case 'mime':
|
|
594
619
|
return handleGetMimeContent(args);
|
|
595
620
|
case 'message':
|
|
596
|
-
default:
|
|
597
621
|
return handleExportEmail(args);
|
|
622
|
+
default:
|
|
623
|
+
return {
|
|
624
|
+
content: [
|
|
625
|
+
{
|
|
626
|
+
type: 'text',
|
|
627
|
+
text: `Unknown export target '${target}'. Valid targets: message, messages, conversation, mime.`,
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
};
|
|
598
631
|
}
|
|
599
632
|
},
|
|
600
633
|
},
|
|
@@ -630,6 +663,7 @@ const emailTools = [
|
|
|
630
663
|
'Comma-separated tip types to request (default: all). Options: automaticReplies, mailboxFullStatus, customMailTip, externalMemberCount, totalMemberCount, maxMessageSize, deliveryRestriction, moderationStatus, recipientScope, recipientSuggestions',
|
|
631
664
|
},
|
|
632
665
|
},
|
|
666
|
+
additionalProperties: false,
|
|
633
667
|
required: ['recipients'],
|
|
634
668
|
},
|
|
635
669
|
handler: handleGetMailTips,
|
package/email/list.js
CHANGED
|
@@ -44,7 +44,12 @@ function getFieldPresetForVerbosity(verbosity) {
|
|
|
44
44
|
*/
|
|
45
45
|
async function handleListEmails(args) {
|
|
46
46
|
const folder = args.folder || 'inbox';
|
|
47
|
-
|
|
47
|
+
// F-17: accept `maxResults` as an alias for `count` here too. The
|
|
48
|
+
// search-mode handler already does this; list-mode used `args.count`
|
|
49
|
+
// only, so callers passing `maxResults=5` to a non-search list call
|
|
50
|
+
// saw their override silently ignored.
|
|
51
|
+
const requestedCount =
|
|
52
|
+
args.count ?? args.maxResults ?? DEFAULT_LIMITS.listEmails;
|
|
48
53
|
const verbosity = args.outputVerbosity || VERBOSITY.STANDARD;
|
|
49
54
|
|
|
50
55
|
try {
|
package/email/mail-tips.js
CHANGED
|
@@ -203,7 +203,7 @@ async function handleGetMailTips(args) {
|
|
|
203
203
|
content: [
|
|
204
204
|
{
|
|
205
205
|
type: 'text',
|
|
206
|
-
text: 'No mail tips returned
|
|
206
|
+
text: 'No mail tips returned. Mail Tips is M365-only — personal Outlook.com accounts return empty responses, so recipient validation is unavailable on this account.',
|
|
207
207
|
},
|
|
208
208
|
],
|
|
209
209
|
};
|
|
@@ -211,15 +211,39 @@ async function handleGetMailTips(args) {
|
|
|
211
211
|
|
|
212
212
|
const { formatted, warningCount } = formatMailTips(mailTips);
|
|
213
213
|
|
|
214
|
+
// F-23: Detect a "fully empty" tips response — every recipient
|
|
215
|
+
// returned with no actionable fields. Personal Outlook.com
|
|
216
|
+
// accounts surface this as a successful empty response rather
|
|
217
|
+
// than a feature-unsupported error, leading to false confidence
|
|
218
|
+
// when callers see "No issues detected".
|
|
219
|
+
const allEmpty = mailTips.every((tip) => {
|
|
220
|
+
const hasContent =
|
|
221
|
+
tip.recipientNotFound ||
|
|
222
|
+
tip.mailboxFull ||
|
|
223
|
+
tip.deliveryRestricted ||
|
|
224
|
+
tip.isModerated ||
|
|
225
|
+
tip.automaticReplies?.message ||
|
|
226
|
+
tip.maxMessageSize ||
|
|
227
|
+
tip.totalMemberCount ||
|
|
228
|
+
tip.customMailTip;
|
|
229
|
+
return !hasContent;
|
|
230
|
+
});
|
|
231
|
+
|
|
214
232
|
let header = `# Mail Tips\n\n`;
|
|
215
233
|
header += `**Recipients checked**: ${mailTips.length}\n`;
|
|
216
|
-
header += `**Warnings**: ${warningCount}\n
|
|
234
|
+
header += `**Warnings**: ${warningCount}\n`;
|
|
235
|
+
if (allEmpty && warningCount === 0) {
|
|
236
|
+
header +=
|
|
237
|
+
'\n**Note**: Graph returned no actionable mail tips for any recipient. This usually means Mail Tips is not supported on the connected account (M365-only feature) — "No issues detected" below means "no warnings flagged by Graph", NOT "validated as deliverable".\n';
|
|
238
|
+
}
|
|
239
|
+
header += '\n';
|
|
217
240
|
|
|
218
241
|
return {
|
|
219
242
|
content: [{ type: 'text', text: header + formatted }],
|
|
220
243
|
_meta: {
|
|
221
244
|
recipientCount: mailTips.length,
|
|
222
245
|
warningCount,
|
|
246
|
+
allEmpty,
|
|
223
247
|
},
|
|
224
248
|
};
|
|
225
249
|
} catch (error) {
|
package/email/search.js
CHANGED
|
@@ -33,7 +33,12 @@ async function handleSearchEmails(args) {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// F-17: accept `maxResults` as an alias for `count` in non-delta mode.
|
|
37
|
+
// The schema declares both, but `maxResults` was only consumed by
|
|
38
|
+
// the delta path, so callers passing `maxResults=5` to a normal
|
|
39
|
+
// search saw their override silently ignored.
|
|
40
|
+
const requestedCount =
|
|
41
|
+
args.count ?? args.maxResults ?? DEFAULT_LIMITS.searchEmails;
|
|
37
42
|
const verbosity = args.outputVerbosity || VERBOSITY.STANDARD;
|
|
38
43
|
const query = args.query || '';
|
|
39
44
|
const from = args.from || '';
|
|
@@ -122,16 +127,40 @@ async function progressiveSearch(
|
|
|
122
127
|
// Track search strategies attempted
|
|
123
128
|
const searchAttempts = [];
|
|
124
129
|
|
|
125
|
-
// 0. If raw KQL query provided, use it directly
|
|
130
|
+
// 0. If raw KQL query provided, use it directly. The kqlQuery branch
|
|
131
|
+
// *terminates* — if Graph returns 0 (or throws), we surface that
|
|
132
|
+
// explicitly rather than falling through to combined-search, which
|
|
133
|
+
// would drop the user's filter and return unrelated recent emails
|
|
134
|
+
// with a misleading "combined-search" strategy line. (#169)
|
|
126
135
|
if (searchTerms.kqlQuery) {
|
|
127
136
|
try {
|
|
128
|
-
|
|
137
|
+
// Pass the user's KQL through as-is. The user is responsible for
|
|
138
|
+
// their own phrase quoting (e.g. `subject:"foo bar"`); we do NOT
|
|
139
|
+
// auto-wrap, which previously produced broken nested quotes like
|
|
140
|
+
// `"subject:"foo bar""` on Graph $search and silently returned
|
|
141
|
+
// recent unfiltered messages. (#169 V37-F-1)
|
|
142
|
+
const trimmedKql = searchTerms.kqlQuery.trim();
|
|
143
|
+
const alreadyQuoted =
|
|
144
|
+
trimmedKql.startsWith('"') && trimmedKql.endsWith('"');
|
|
145
|
+
const looksLikeExpression =
|
|
146
|
+
trimmedKql.includes(':') || /\s/.test(trimmedKql);
|
|
147
|
+
// Already-quoted phrases and KQL-looking expressions (field syntax
|
|
148
|
+
// or multi-word) are passed through as-is; only bare single tokens
|
|
149
|
+
// are wrapped so Graph treats them as phrase searches.
|
|
150
|
+
let kqlForSearch;
|
|
151
|
+
if (alreadyQuoted || looksLikeExpression) {
|
|
152
|
+
kqlForSearch = trimmedKql;
|
|
153
|
+
} else {
|
|
154
|
+
kqlForSearch = `"${trimmedKql}"`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.error(`Attempting raw KQL search: ${kqlForSearch}`);
|
|
129
158
|
searchAttempts.push('raw-kql');
|
|
130
159
|
|
|
131
160
|
const kqlParams = {
|
|
132
161
|
$top: Math.min(50, maxCount),
|
|
133
162
|
$select: selectFields,
|
|
134
|
-
$search:
|
|
163
|
+
$search: kqlForSearch,
|
|
135
164
|
};
|
|
136
165
|
|
|
137
166
|
const response = await callGraphAPIPaginated(
|
|
@@ -141,20 +170,40 @@ async function progressiveSearch(
|
|
|
141
170
|
kqlParams,
|
|
142
171
|
maxCount
|
|
143
172
|
);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
console.error(
|
|
174
|
+
`Raw KQL search complete: ${response.value?.length || 0} results`
|
|
175
|
+
);
|
|
176
|
+
const matched = response.value?.length || 0;
|
|
177
|
+
response._searchInfo = {
|
|
178
|
+
attemptsCount: searchAttempts.length,
|
|
179
|
+
strategies: searchAttempts,
|
|
180
|
+
originalTerms: searchTerms,
|
|
181
|
+
filterTerms: filterTerms,
|
|
182
|
+
kqlApplied: kqlForSearch,
|
|
183
|
+
// noResults flips on the helpful "Suggestions" block in the
|
|
184
|
+
// formatter — without it, an empty kqlQuery result would render
|
|
185
|
+
// the bare "No emails found matching your search criteria" line
|
|
186
|
+
// with no guidance.
|
|
187
|
+
noResults: matched === 0,
|
|
188
|
+
};
|
|
189
|
+
// Always return — never silently fall through to a path that
|
|
190
|
+
// would ignore kqlQuery and return unrelated emails.
|
|
191
|
+
return response;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error(`Raw KQL search failed: ${error.message}`);
|
|
194
|
+
// Surface the failure rather than masking it with unrelated results.
|
|
195
|
+
searchAttempts.push('raw-kql-error');
|
|
196
|
+
return {
|
|
197
|
+
value: [],
|
|
198
|
+
_searchInfo: {
|
|
149
199
|
attemptsCount: searchAttempts.length,
|
|
150
200
|
strategies: searchAttempts,
|
|
151
201
|
originalTerms: searchTerms,
|
|
152
202
|
filterTerms: filterTerms,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
console.error(`Raw KQL search failed: ${error.message}`);
|
|
203
|
+
kqlError: error.message,
|
|
204
|
+
noResults: true,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
158
207
|
}
|
|
159
208
|
}
|
|
160
209
|
|
|
@@ -566,18 +615,26 @@ function filterToClientSide(messages, toValue) {
|
|
|
566
615
|
* @returns {Array} - Filtered messages matching the query
|
|
567
616
|
*/
|
|
568
617
|
function filterQueryClientSide(messages, queryText) {
|
|
569
|
-
|
|
618
|
+
// F-12: split multi-word queries on whitespace and require ALL words
|
|
619
|
+
// to be present (AND search). Previous behaviour was substring match
|
|
620
|
+
// on the literal phrase, which missed the common case where the
|
|
621
|
+
// user types e.g. "github token" expecting it to find a subject
|
|
622
|
+
// like "[GitHub] Your fine-grained personal access token".
|
|
623
|
+
const queryLower = queryText.toLowerCase().trim();
|
|
624
|
+
if (!queryLower) return messages;
|
|
625
|
+
const words = queryLower.split(/\s+/).filter(Boolean);
|
|
626
|
+
|
|
570
627
|
return messages.filter((m) => {
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
);
|
|
628
|
+
const haystack = [
|
|
629
|
+
m.subject,
|
|
630
|
+
m.bodyPreview,
|
|
631
|
+
m.from?.emailAddress?.address,
|
|
632
|
+
m.from?.emailAddress?.name,
|
|
633
|
+
]
|
|
634
|
+
.filter(Boolean)
|
|
635
|
+
.join(' ')
|
|
636
|
+
.toLowerCase();
|
|
637
|
+
return words.every((w) => haystack.includes(w));
|
|
581
638
|
});
|
|
582
639
|
}
|
|
583
640
|
|
package/folder/create.js
CHANGED
|
@@ -43,6 +43,9 @@ async function handleCreateFolder(args) {
|
|
|
43
43
|
text: result.message,
|
|
44
44
|
},
|
|
45
45
|
],
|
|
46
|
+
// F-31: surface the folder ID in _meta so callers can chain
|
|
47
|
+
// create→move→stats without an extra `folders list` round-trip.
|
|
48
|
+
...(result.folderId && { _meta: { folderId: result.folderId } }),
|
|
46
49
|
};
|
|
47
50
|
} catch (error) {
|
|
48
51
|
if (error.message === 'Authentication required') {
|
|
@@ -118,7 +121,9 @@ async function createMailFolder(accessToken, folderName, parentFolderName) {
|
|
|
118
121
|
|
|
119
122
|
return {
|
|
120
123
|
success: true,
|
|
121
|
-
|
|
124
|
+
// F-31: include the ID in the human-readable message too so it
|
|
125
|
+
// shows up for AI agents that don't read _meta.
|
|
126
|
+
message: `Successfully created folder "${folderName}" ${locationInfo}.\n\n**ID**: ${response.id}`,
|
|
122
127
|
folderId: response.id,
|
|
123
128
|
};
|
|
124
129
|
} else {
|
package/folder/index.js
CHANGED
|
@@ -81,6 +81,7 @@ const folderTools = [
|
|
|
81
81
|
'Folder name to delete — resolved to ID (action=delete). Cannot delete protected folders (Inbox, Drafts, Sent, etc.)',
|
|
82
82
|
},
|
|
83
83
|
},
|
|
84
|
+
additionalProperties: false,
|
|
84
85
|
required: [],
|
|
85
86
|
},
|
|
86
87
|
handler: async (args) => {
|
|
@@ -95,8 +96,16 @@ const folderTools = [
|
|
|
95
96
|
case 'delete':
|
|
96
97
|
return handleDeleteFolder(args);
|
|
97
98
|
case 'list':
|
|
98
|
-
default:
|
|
99
99
|
return handleListFolders(args);
|
|
100
|
+
default:
|
|
101
|
+
return {
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: 'text',
|
|
105
|
+
text: `Unknown action '${action}'. Valid actions: list, create, move, stats, delete.`,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
100
109
|
}
|
|
101
110
|
},
|
|
102
111
|
},
|
package/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
StdioServerTransport,
|
|
11
11
|
} = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
12
12
|
const config = require('./config');
|
|
13
|
+
const { coerceArgsAgainstSchema } = require('./utils/schema-coerce');
|
|
13
14
|
|
|
14
15
|
// Import module tools
|
|
15
16
|
const { authTools, setToolCount } = require('./auth');
|
|
@@ -26,6 +27,19 @@ const { advancedTools } = require('./advanced');
|
|
|
26
27
|
console.error(`STARTING ${config.SERVER_NAME.toUpperCase()} MCP SERVER`);
|
|
27
28
|
console.error(`Test mode is ${config.USE_TEST_MODE ? 'enabled' : 'disabled'}`);
|
|
28
29
|
|
|
30
|
+
// F-1 / F-48: warn at startup when safety belts are unset. Mirrors the
|
|
31
|
+
// warning surfaced by `auth action=about`. Visible to operators reading
|
|
32
|
+
// stderr; AI clients reading the JSON-RPC stream are unaffected.
|
|
33
|
+
if (
|
|
34
|
+
!process.env.OUTLOOK_MAX_EMAILS_PER_SESSION &&
|
|
35
|
+
!process.env.OUTLOOK_ALLOWED_RECIPIENTS &&
|
|
36
|
+
!config.USE_TEST_MODE
|
|
37
|
+
) {
|
|
38
|
+
console.error(
|
|
39
|
+
'⚠ Safety belts not configured. Consider setting OUTLOOK_MAX_EMAILS_PER_SESSION and OUTLOOK_ALLOWED_RECIPIENTS in your .mcp.json env block for safer AI-assisted sending. See `auth action=about` for details.'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
// Combine all tools
|
|
30
44
|
const TOOLS = [
|
|
31
45
|
...authTools,
|
|
@@ -110,6 +124,25 @@ server.fallbackRequestHandler = async (request) => {
|
|
|
110
124
|
const tool = TOOLS.find((t) => t.name === name);
|
|
111
125
|
|
|
112
126
|
if (tool && tool.handler) {
|
|
127
|
+
// Coerce + validate args against the tool's inputSchema before
|
|
128
|
+
// dispatching. Catches array-as-string, boolean-as-string, unknown
|
|
129
|
+
// params, and out-of-enum action values at the MCP boundary so
|
|
130
|
+
// handlers receive properly-typed JS values. (#160, #162)
|
|
131
|
+
if (tool.inputSchema) {
|
|
132
|
+
const coerced = coerceArgsAgainstSchema(args, tool.inputSchema);
|
|
133
|
+
if (coerced.error) {
|
|
134
|
+
return {
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: 'text',
|
|
138
|
+
text: `Invalid arguments for tool '${name}':\n${coerced.error}`,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
isError: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return await tool.handler(coerced.args);
|
|
145
|
+
}
|
|
113
146
|
return await tool.handler(args);
|
|
114
147
|
}
|
|
115
148
|
|
package/llms.txt
CHANGED
|
@@ -22,7 +22,8 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
22
22
|
|
|
23
23
|
## Key Differentiators
|
|
24
24
|
|
|
25
|
-
- **Progressive search**: Automatically falls back through 4 search strategies when Microsoft's `$search` API is unavailable (personal accounts) — most Graph API wrappers fail silently
|
|
25
|
+
- **Progressive search**: Automatically falls back through 4 search strategies when Microsoft's `$search` API is unavailable (personal accounts) — most Graph API wrappers fail silently. Explicit "no results" messaging instead of unfiltered fallback.
|
|
26
|
+
- **Remote-friendly auth**: Device code flow (default) — no auth server, no port forwarding, no SSH tunnels. State persists across MCP server restarts. Works from Untether, mosh, SSH, and headless environments.
|
|
26
27
|
- **Email forensics**: Built-in header analysis for DKIM, SPF, DMARC authentication, delivery chains, and spam scores — useful for phishing investigation and compliance
|
|
27
28
|
- **Delta sync**: Incremental inbox monitoring — returns only new, modified, and deleted emails since last check, with tokens for continuous polling
|
|
28
29
|
- **Batch operations**: Flag, move, export, or categorise multiple emails in a single tool call; search-driven export for batch archiving without collecting IDs
|
|
@@ -72,7 +73,12 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
|
|
|
72
73
|
|
|
73
74
|
- [README](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/README.md): Full documentation including setup, Azure configuration, and usage
|
|
74
75
|
- [Tools Reference](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/quickrefs/tools-reference.md): All 22 tools with parameters and safety annotations
|
|
76
|
+
- [Connect Outlook to Claude](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/how-to/getting-started/connect-outlook-to-claude.md): Step-by-step setup guide for Claude Desktop / Claude Code
|
|
77
|
+
- [Verify Your Connection](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/how-to/getting-started/verify-your-connection.md): Test and troubleshoot the connection after installation
|
|
78
|
+
- [Azure Setup](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/guides/azure-setup.md): Azure app registration and API permissions walkthrough
|
|
79
|
+
- [FAQ](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/faq/index.md): Frequently asked questions — install, accounts, permissions, tokens, send safety, updates, uninstall (also at <https://littlebearapps.com/help/outlook-assistant/faq/>)
|
|
75
80
|
- [CLAUDE.md](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CLAUDE.md): Quick reference for development
|
|
76
81
|
- [CONTRIBUTING](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CONTRIBUTING.md): Contribution guidelines
|
|
77
|
-
- [CHANGELOG](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CHANGELOG.md): Version history
|
|
82
|
+
- [CHANGELOG](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CHANGELOG.md): Version history (current: v3.7.4 — patch release closing two regressions surfaced by an independent v3.7.3 E2E re-verification: F-24 chokepoint now catches JSON-stringified arrays from MCP transport (#168) and search-emails kqlQuery no longer silently drops on Step 0 fall-through (#169))
|
|
83
|
+
- [ROADMAP](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/ROADMAP.md): Active milestones (v3.7.5 polish, v3.8.0 task integration & auth, v3.9.0 new Graph APIs)
|
|
78
84
|
- [SECURITY](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/SECURITY.md): Security policy, token handling, and MCP safety controls
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebearapps/outlook-assistant",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.4",
|
|
4
4
|
"mcpName": "io.github.littlebearapps/outlook-assistant",
|
|
5
5
|
"description": "Outlook Assistant — MCP server with 22 tools for email, calendar, contacts, and settings via Microsoft Graph API",
|
|
6
6
|
"main": "index.js",
|
package/rules/create.js
CHANGED
|
@@ -150,7 +150,10 @@ async function handleCreateRule(args) {
|
|
|
150
150
|
);
|
|
151
151
|
|
|
152
152
|
if (response && response.id) {
|
|
153
|
-
|
|
153
|
+
// F-43: include the rule ID. update/delete accept ruleName so this
|
|
154
|
+
// is workable, but ID is more reliable when names contain unicode
|
|
155
|
+
// or duplicates exist.
|
|
156
|
+
let text = `Successfully created rule "${name}" with sequence ${ruleSequence}.\n\n**ID**: ${response.id}`;
|
|
154
157
|
if (allWarnings.length > 0) {
|
|
155
158
|
text += `\n\nNotes:\n${allWarnings.map((w) => `- ${w}`).join('\n')}`;
|
|
156
159
|
}
|
|
@@ -160,6 +163,7 @@ async function handleCreateRule(args) {
|
|
|
160
163
|
}
|
|
161
164
|
return {
|
|
162
165
|
content: [{ type: 'text', text }],
|
|
166
|
+
_meta: { ruleId: response.id },
|
|
163
167
|
};
|
|
164
168
|
}
|
|
165
169
|
|
package/rules/index.js
CHANGED
|
@@ -209,6 +209,11 @@ const rulesTools = [
|
|
|
209
209
|
description:
|
|
210
210
|
'Rule name (action=create required, action=update to rename)',
|
|
211
211
|
},
|
|
212
|
+
displayName: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
description:
|
|
215
|
+
"Alias for `name` (matches Graph's own `displayName` field).",
|
|
216
|
+
},
|
|
212
217
|
dryRun: {
|
|
213
218
|
type: 'boolean',
|
|
214
219
|
description:
|
|
@@ -378,10 +383,15 @@ const rulesTools = [
|
|
|
378
383
|
description: 'ID of existing rule (action=update/delete)',
|
|
379
384
|
},
|
|
380
385
|
},
|
|
386
|
+
additionalProperties: false,
|
|
381
387
|
required: [],
|
|
382
388
|
},
|
|
383
389
|
handler: async (args) => {
|
|
384
390
|
const action = args.action || 'list';
|
|
391
|
+
// F-41: accept Graph's own `displayName` as alias for `name`.
|
|
392
|
+
if (args.displayName && !args.name) {
|
|
393
|
+
args = { ...args, name: args.displayName };
|
|
394
|
+
}
|
|
385
395
|
switch (action) {
|
|
386
396
|
case 'create':
|
|
387
397
|
return handleCreateRule(args);
|
|
@@ -392,8 +402,16 @@ const rulesTools = [
|
|
|
392
402
|
case 'delete':
|
|
393
403
|
return handleDeleteRule(args);
|
|
394
404
|
case 'list':
|
|
395
|
-
default:
|
|
396
405
|
return handleListRules(args);
|
|
406
|
+
default:
|
|
407
|
+
return {
|
|
408
|
+
content: [
|
|
409
|
+
{
|
|
410
|
+
type: 'text',
|
|
411
|
+
text: `Unknown action '${action}'. Valid actions: list, create, update, reorder, delete.`,
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
};
|
|
397
415
|
}
|
|
398
416
|
},
|
|
399
417
|
},
|
package/rules/update.js
CHANGED
|
@@ -163,11 +163,19 @@ async function handleUpdateRule(args) {
|
|
|
163
163
|
|
|
164
164
|
const changedFields = Object.keys(patch)
|
|
165
165
|
.map((k) => {
|
|
166
|
-
if (k === 'displayName')
|
|
166
|
+
if (k === 'displayName') {
|
|
167
|
+
return `name: "${currentRule.displayName}" → "${patch.displayName}"`;
|
|
168
|
+
}
|
|
167
169
|
if (k === 'isEnabled') {
|
|
168
|
-
|
|
170
|
+
// Show explicit before/after to remove the F-45 ambiguity:
|
|
171
|
+
// previously rendered as bare "enabled"/"disabled" with no
|
|
172
|
+
// indication of direction, so callers couldn't tell whether
|
|
173
|
+
// the rule was enabled or whether the action just succeeded.
|
|
174
|
+
return `isEnabled: ${currentRule.isEnabled} → ${patch.isEnabled}`;
|
|
175
|
+
}
|
|
176
|
+
if (k === 'sequence') {
|
|
177
|
+
return `sequence: ${currentRule.sequence} → ${patch.sequence}`;
|
|
169
178
|
}
|
|
170
|
-
if (k === 'sequence') return `sequence → ${patch.sequence}`;
|
|
171
179
|
if (k === 'conditions') return 'conditions updated';
|
|
172
180
|
if (k === 'actions') return 'actions updated';
|
|
173
181
|
if (k === 'exceptions') return 'exceptions updated';
|