@littlebearapps/outlook-assistant 3.6.0 → 3.7.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 +1 -1
- package/advanced/index.js +5 -5
- package/auth/device-code.js +3 -1
- package/auth/oauth-server.js +4 -4
- package/auth/token-manager.js +2 -2
- package/calendar/list.js +3 -3
- package/categories/index.js +4 -4
- package/contacts/index.js +11 -18
- package/email/conversations.js +19 -17
- package/email/delta.js +1 -1
- package/email/draft.js +6 -8
- package/email/folder-utils.js +1 -1
- package/email/headers.js +10 -10
- package/email/list.js +1 -1
- package/email/mime.js +13 -12
- package/email/search.js +280 -24
- package/email/send.js +1 -1
- package/folder/list.js +1 -1
- package/llms.txt +2 -1
- package/package.json +1 -1
- package/rules/create.js +85 -161
- package/rules/index.js +162 -29
- package/rules/list.js +144 -62
- package/rules/rule-builder.js +284 -0
- package/rules/update.js +213 -0
- package/settings/index.js +10 -22
- package/utils/graph-api.js +1 -1
- package/utils/response-formatter.js +8 -6
- package/utils/safety.js +113 -0
package/email/search.js
CHANGED
|
@@ -145,6 +145,12 @@ async function progressiveSearch(
|
|
|
145
145
|
console.error(
|
|
146
146
|
`Raw KQL search successful: found ${response.value.length} results`
|
|
147
147
|
);
|
|
148
|
+
response._searchInfo = {
|
|
149
|
+
attemptsCount: searchAttempts.length,
|
|
150
|
+
strategies: searchAttempts,
|
|
151
|
+
originalTerms: searchTerms,
|
|
152
|
+
filterTerms: filterTerms,
|
|
153
|
+
};
|
|
148
154
|
return response;
|
|
149
155
|
}
|
|
150
156
|
} catch (error) {
|
|
@@ -166,7 +172,7 @@ async function progressiveSearch(
|
|
|
166
172
|
) {
|
|
167
173
|
// Skip directly to boolean-only filter (step 3) — combined search is redundant
|
|
168
174
|
console.error('Only boolean filters provided, skipping combined search');
|
|
169
|
-
} else
|
|
175
|
+
} else {
|
|
170
176
|
try {
|
|
171
177
|
const params = buildSearchParams(
|
|
172
178
|
searchTerms,
|
|
@@ -188,11 +194,18 @@ async function progressiveSearch(
|
|
|
188
194
|
console.error(
|
|
189
195
|
`Combined search successful: found ${response.value.length} results`
|
|
190
196
|
);
|
|
197
|
+
response._searchInfo = {
|
|
198
|
+
attemptsCount: searchAttempts.length,
|
|
199
|
+
strategies: searchAttempts,
|
|
200
|
+
originalTerms: searchTerms,
|
|
201
|
+
filterTerms: filterTerms,
|
|
202
|
+
};
|
|
191
203
|
return response;
|
|
192
204
|
}
|
|
193
205
|
} catch (error) {
|
|
194
206
|
console.error(`Combined search failed: ${error.message}`);
|
|
195
207
|
}
|
|
208
|
+
}
|
|
196
209
|
|
|
197
210
|
// 2. Try each search term individually, starting with most specific
|
|
198
211
|
const searchPriority = ['from', 'to', 'subject', 'query'];
|
|
@@ -240,10 +253,109 @@ async function progressiveSearch(
|
|
|
240
253
|
console.error(
|
|
241
254
|
`Search with ${term} successful: found ${response.value.length} results`
|
|
242
255
|
);
|
|
256
|
+
response._searchInfo = {
|
|
257
|
+
attemptsCount: searchAttempts.length,
|
|
258
|
+
strategies: searchAttempts,
|
|
259
|
+
originalTerms: searchTerms,
|
|
260
|
+
filterTerms: filterTerms,
|
|
261
|
+
};
|
|
243
262
|
return response;
|
|
244
263
|
}
|
|
264
|
+
|
|
265
|
+
// Client-side fallback for 'to' filter — toRecipients/any() lambda
|
|
266
|
+
// returns 0 results on personal accounts even when emails exist
|
|
267
|
+
if (term === 'to') {
|
|
268
|
+
console.error(
|
|
269
|
+
'to filter returned 0 results, trying client-side filtering'
|
|
270
|
+
);
|
|
271
|
+
searchAttempts.push('client-side-to');
|
|
272
|
+
const messages = await fetchForClientSideFilter(
|
|
273
|
+
accessToken,
|
|
274
|
+
endpoint,
|
|
275
|
+
maxCount
|
|
276
|
+
);
|
|
277
|
+
const matched = filterToClientSide(messages, searchTerms[term]);
|
|
278
|
+
if (matched.length > 0) {
|
|
279
|
+
console.error(
|
|
280
|
+
`Client-side to filter matched ${matched.length} of ${messages.length} messages`
|
|
281
|
+
);
|
|
282
|
+
return { value: matched.slice(0, maxCount) };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Client-side fallback for 'query' — search bodyPreview, subject, from
|
|
287
|
+
if (term === 'query') {
|
|
288
|
+
console.error(
|
|
289
|
+
'query contains(subject) returned 0 results, trying client-side body search'
|
|
290
|
+
);
|
|
291
|
+
searchAttempts.push('client-side-query');
|
|
292
|
+
const messages = await fetchForClientSideFilter(
|
|
293
|
+
accessToken,
|
|
294
|
+
endpoint,
|
|
295
|
+
maxCount
|
|
296
|
+
);
|
|
297
|
+
const matched = filterQueryClientSide(messages, searchTerms[term]);
|
|
298
|
+
if (matched.length > 0) {
|
|
299
|
+
console.error(
|
|
300
|
+
`Client-side query matched ${matched.length} of ${messages.length} messages`
|
|
301
|
+
);
|
|
302
|
+
return { value: matched.slice(0, maxCount) };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
245
305
|
} catch (error) {
|
|
246
306
|
console.error(`Search with ${term} failed: ${error.message}`);
|
|
307
|
+
|
|
308
|
+
// Client-side fallback for 'to' when API throws (e.g. InefficientFilter)
|
|
309
|
+
if (term === 'to') {
|
|
310
|
+
try {
|
|
311
|
+
console.error(
|
|
312
|
+
'to filter threw error, trying client-side filtering'
|
|
313
|
+
);
|
|
314
|
+
searchAttempts.push('client-side-to');
|
|
315
|
+
const messages = await fetchForClientSideFilter(
|
|
316
|
+
accessToken,
|
|
317
|
+
endpoint,
|
|
318
|
+
maxCount
|
|
319
|
+
);
|
|
320
|
+
const matched = filterToClientSide(messages, searchTerms[term]);
|
|
321
|
+
if (matched.length > 0) {
|
|
322
|
+
console.error(
|
|
323
|
+
`Client-side to filter matched ${matched.length} of ${messages.length} messages`
|
|
324
|
+
);
|
|
325
|
+
return { value: matched.slice(0, maxCount) };
|
|
326
|
+
}
|
|
327
|
+
} catch (fallbackError) {
|
|
328
|
+
console.error(
|
|
329
|
+
`Client-side to fallback also failed: ${fallbackError.message}`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Client-side fallback for 'query' when API throws
|
|
335
|
+
if (term === 'query') {
|
|
336
|
+
try {
|
|
337
|
+
console.error(
|
|
338
|
+
'query filter threw error, trying client-side body search'
|
|
339
|
+
);
|
|
340
|
+
searchAttempts.push('client-side-query');
|
|
341
|
+
const messages = await fetchForClientSideFilter(
|
|
342
|
+
accessToken,
|
|
343
|
+
endpoint,
|
|
344
|
+
maxCount
|
|
345
|
+
);
|
|
346
|
+
const matched = filterQueryClientSide(messages, searchTerms[term]);
|
|
347
|
+
if (matched.length > 0) {
|
|
348
|
+
console.error(
|
|
349
|
+
`Client-side query matched ${matched.length} of ${messages.length} messages`
|
|
350
|
+
);
|
|
351
|
+
return { value: matched.slice(0, maxCount) };
|
|
352
|
+
}
|
|
353
|
+
} catch (fallbackError) {
|
|
354
|
+
console.error(
|
|
355
|
+
`Client-side query fallback also failed: ${fallbackError.message}`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
247
359
|
}
|
|
248
360
|
}
|
|
249
361
|
}
|
|
@@ -281,6 +393,12 @@ async function progressiveSearch(
|
|
|
281
393
|
console.error(
|
|
282
394
|
`Boolean filter search found ${response.value?.length || 0} results`
|
|
283
395
|
);
|
|
396
|
+
response._searchInfo = {
|
|
397
|
+
attemptsCount: searchAttempts.length,
|
|
398
|
+
strategies: searchAttempts,
|
|
399
|
+
originalTerms: searchTerms,
|
|
400
|
+
filterTerms: filterTerms,
|
|
401
|
+
};
|
|
284
402
|
return response;
|
|
285
403
|
} catch (error) {
|
|
286
404
|
console.error(`Boolean filter search failed: ${error.message}`);
|
|
@@ -300,6 +418,12 @@ async function progressiveSearch(
|
|
|
300
418
|
retryParams,
|
|
301
419
|
maxCount
|
|
302
420
|
);
|
|
421
|
+
response._searchInfo = {
|
|
422
|
+
attemptsCount: searchAttempts.length,
|
|
423
|
+
strategies: searchAttempts,
|
|
424
|
+
originalTerms: searchTerms,
|
|
425
|
+
filterTerms: filterTerms,
|
|
426
|
+
};
|
|
303
427
|
return response;
|
|
304
428
|
} catch (retryError) {
|
|
305
429
|
console.error(
|
|
@@ -310,8 +434,35 @@ async function progressiveSearch(
|
|
|
310
434
|
}
|
|
311
435
|
}
|
|
312
436
|
|
|
313
|
-
// 4. Final fallback
|
|
314
|
-
|
|
437
|
+
// 4. Final fallback
|
|
438
|
+
// If the user specified search filters, return 0 results with guidance
|
|
439
|
+
// instead of silently returning unfiltered recent emails.
|
|
440
|
+
const hasAnyFilters =
|
|
441
|
+
searchTerms.query ||
|
|
442
|
+
searchTerms.from ||
|
|
443
|
+
searchTerms.to ||
|
|
444
|
+
searchTerms.subject ||
|
|
445
|
+
searchTerms.kqlQuery;
|
|
446
|
+
|
|
447
|
+
if (hasAnyFilters) {
|
|
448
|
+
console.error(
|
|
449
|
+
'All search strategies exhausted with filters active — returning 0 results'
|
|
450
|
+
);
|
|
451
|
+
searchAttempts.push('no-results');
|
|
452
|
+
return {
|
|
453
|
+
value: [],
|
|
454
|
+
_searchInfo: {
|
|
455
|
+
attemptsCount: searchAttempts.length,
|
|
456
|
+
strategies: searchAttempts,
|
|
457
|
+
originalTerms: searchTerms,
|
|
458
|
+
filterTerms: filterTerms,
|
|
459
|
+
noResults: true,
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// No search filters specified — return recent emails (list mode)
|
|
465
|
+
console.error('No search filters specified, returning recent emails');
|
|
315
466
|
searchAttempts.push('recent-emails');
|
|
316
467
|
|
|
317
468
|
const basicParams = {
|
|
@@ -327,11 +478,8 @@ async function progressiveSearch(
|
|
|
327
478
|
basicParams,
|
|
328
479
|
maxCount
|
|
329
480
|
);
|
|
330
|
-
console.error(
|
|
331
|
-
`Fallback to recent emails found ${response.value?.length || 0} results`
|
|
332
|
-
);
|
|
481
|
+
console.error(`Recent emails: found ${response.value?.length || 0} results`);
|
|
333
482
|
|
|
334
|
-
// Add a note to the response about the search attempts
|
|
335
483
|
response._searchInfo = {
|
|
336
484
|
attemptsCount: searchAttempts.length,
|
|
337
485
|
strategies: searchAttempts,
|
|
@@ -390,6 +538,74 @@ function buildToFilter(val) {
|
|
|
390
538
|
return `toRecipients/any(r: contains(r/emailAddress/name, '${val}'))`;
|
|
391
539
|
}
|
|
392
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Client-side filter for toRecipients — used when the OData toRecipients/any()
|
|
543
|
+
* lambda expression fails on personal accounts (InefficientFilter).
|
|
544
|
+
* Mirrors the pattern from conversations.js lines 138-147.
|
|
545
|
+
* @param {Array} messages - Array of message objects with toRecipients
|
|
546
|
+
* @param {string} toValue - The to filter value (email, domain, or name)
|
|
547
|
+
* @returns {Array} - Filtered messages where at least one recipient matches
|
|
548
|
+
*/
|
|
549
|
+
function filterToClientSide(messages, toValue) {
|
|
550
|
+
const toLower = toValue.toLowerCase();
|
|
551
|
+
return messages.filter((m) =>
|
|
552
|
+
(m.toRecipients || []).some((r) => {
|
|
553
|
+
const addr = (r.emailAddress?.address || '').toLowerCase();
|
|
554
|
+
const name = (r.emailAddress?.name || '').toLowerCase();
|
|
555
|
+
return addr.includes(toLower) || name.includes(toLower);
|
|
556
|
+
})
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Client-side filter for free-text query — used when $search and
|
|
562
|
+
* contains(subject) both fail on personal accounts.
|
|
563
|
+
* Searches subject, bodyPreview, from address, and from name.
|
|
564
|
+
* @param {Array} messages - Array of message objects
|
|
565
|
+
* @param {string} queryText - The query text to search for
|
|
566
|
+
* @returns {Array} - Filtered messages matching the query
|
|
567
|
+
*/
|
|
568
|
+
function filterQueryClientSide(messages, queryText) {
|
|
569
|
+
const queryLower = queryText.toLowerCase();
|
|
570
|
+
return messages.filter((m) => {
|
|
571
|
+
const subject = (m.subject || '').toLowerCase();
|
|
572
|
+
const body = (m.bodyPreview || '').toLowerCase();
|
|
573
|
+
const fromAddr = (m.from?.emailAddress?.address || '').toLowerCase();
|
|
574
|
+
const fromName = (m.from?.emailAddress?.name || '').toLowerCase();
|
|
575
|
+
return (
|
|
576
|
+
subject.includes(queryLower) ||
|
|
577
|
+
body.includes(queryLower) ||
|
|
578
|
+
fromAddr.includes(queryLower) ||
|
|
579
|
+
fromName.includes(queryLower)
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Fetch recent messages for client-side filtering fallback.
|
|
586
|
+
* Uses the 'search' field preset which includes toRecipients and bodyPreview.
|
|
587
|
+
* @param {string} accessToken - Access token
|
|
588
|
+
* @param {string} endpoint - API endpoint
|
|
589
|
+
* @param {number} maxCount - Maximum results to fetch
|
|
590
|
+
* @returns {Promise<Array>} - Array of message objects
|
|
591
|
+
*/
|
|
592
|
+
async function fetchForClientSideFilter(accessToken, endpoint, maxCount) {
|
|
593
|
+
const searchFields = getEmailFields('search');
|
|
594
|
+
const params = {
|
|
595
|
+
$top: Math.min(200, maxCount * 5),
|
|
596
|
+
$select: searchFields,
|
|
597
|
+
$orderby: 'receivedDateTime desc',
|
|
598
|
+
};
|
|
599
|
+
const response = await callGraphAPIPaginated(
|
|
600
|
+
accessToken,
|
|
601
|
+
'GET',
|
|
602
|
+
endpoint,
|
|
603
|
+
params,
|
|
604
|
+
Math.min(200, maxCount * 5)
|
|
605
|
+
);
|
|
606
|
+
return response.value || [];
|
|
607
|
+
}
|
|
608
|
+
|
|
393
609
|
/**
|
|
394
610
|
* Build search parameters from search terms and filter terms
|
|
395
611
|
* Uses $filter for email addresses (more reliable than $search)
|
|
@@ -530,37 +746,75 @@ function addBooleanFilters(params, filterTerms) {
|
|
|
530
746
|
* @returns {object} - MCP response object
|
|
531
747
|
*/
|
|
532
748
|
function formatSearchResults(response, folder, verbosity) {
|
|
749
|
+
// Build metadata
|
|
750
|
+
const meta = {
|
|
751
|
+
returned: (response.value || []).length,
|
|
752
|
+
totalAvailable: response['@odata.count'] || null,
|
|
753
|
+
hasMore: Boolean(response['@odata.nextLink']),
|
|
754
|
+
verbosity: verbosity,
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// Add searchMetadata to _meta when available (for programmatic fallback detection)
|
|
758
|
+
if (response._searchInfo) {
|
|
759
|
+
const finalStrategy =
|
|
760
|
+
response._searchInfo.strategies[
|
|
761
|
+
response._searchInfo.strategies.length - 1
|
|
762
|
+
];
|
|
763
|
+
meta.searchMetadata = {
|
|
764
|
+
strategiesAttempted: response._searchInfo.strategies,
|
|
765
|
+
finalStrategy: finalStrategy,
|
|
766
|
+
filterApplied: !response._searchInfo.noResults,
|
|
767
|
+
originalFilters: response._searchInfo.originalTerms,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Handle 0 results
|
|
533
772
|
if (!response.value || response.value.length === 0) {
|
|
773
|
+
// Actionable guidance when filters were specified but matched nothing
|
|
774
|
+
if (response._searchInfo?.noResults) {
|
|
775
|
+
const filters = response._searchInfo.originalTerms || {};
|
|
776
|
+
const activeFilters = Object.entries(filters)
|
|
777
|
+
.filter(([, v]) => v)
|
|
778
|
+
.map(([k]) => k);
|
|
779
|
+
const filterDesc =
|
|
780
|
+
activeFilters.length > 0
|
|
781
|
+
? ` (filters: ${activeFilters.join(', ')})`
|
|
782
|
+
: '';
|
|
783
|
+
|
|
784
|
+
const text =
|
|
785
|
+
`No emails found matching your filters in "${folder}"${filterDesc}.\n\n` +
|
|
786
|
+
'**Suggestions:**\n' +
|
|
787
|
+
'- Try `searchAllFolders: true` to search across all folders including Archive\n' +
|
|
788
|
+
'- Specify the correct folder if emails have been moved (use `folders` tool to list folders)\n' +
|
|
789
|
+
'- Use `from` filter instead of `to` (more reliable on personal accounts)\n' +
|
|
790
|
+
'- Use `kqlQuery` with `searchAllFolders: true` for cross-folder search';
|
|
791
|
+
|
|
792
|
+
return {
|
|
793
|
+
content: [{ type: 'text', text }],
|
|
794
|
+
_meta: meta,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
534
798
|
return {
|
|
535
799
|
content: [
|
|
536
800
|
{
|
|
537
801
|
type: 'text',
|
|
538
|
-
text:
|
|
802
|
+
text: 'No emails found matching your search criteria.',
|
|
539
803
|
},
|
|
540
804
|
],
|
|
805
|
+
_meta: meta,
|
|
541
806
|
};
|
|
542
807
|
}
|
|
543
808
|
|
|
544
|
-
//
|
|
545
|
-
const meta = {
|
|
546
|
-
returned: response.value.length,
|
|
547
|
-
totalAvailable: response['@odata.count'] || null,
|
|
548
|
-
hasMore: !!response['@odata.nextLink'],
|
|
549
|
-
verbosity: verbosity,
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
// Add search strategy info if available (for debugging)
|
|
809
|
+
// Add search strategy note for transparency
|
|
553
810
|
let searchNote = '';
|
|
554
811
|
if (response._searchInfo) {
|
|
555
|
-
const strategy =
|
|
556
|
-
response._searchInfo.strategies[
|
|
557
|
-
response._searchInfo.strategies.length - 1
|
|
558
|
-
];
|
|
812
|
+
const strategy = meta.searchMetadata.finalStrategy;
|
|
559
813
|
if (strategy === 'recent-emails') {
|
|
560
814
|
searchNote =
|
|
561
|
-
'\n\n**Note**:
|
|
562
|
-
|
|
563
|
-
|
|
815
|
+
'\n\n**Note**: No search filters were applied — showing recent emails.';
|
|
816
|
+
} else if (strategy.startsWith('client-side-')) {
|
|
817
|
+
searchNote = `\n\n_Search strategy: ${strategy} (filtered locally due to personal account API limitations)_`;
|
|
564
818
|
} else {
|
|
565
819
|
searchNote = `\n\n_Search strategy: ${strategy}_`;
|
|
566
820
|
}
|
|
@@ -698,4 +952,6 @@ module.exports = {
|
|
|
698
952
|
buildFromFilter,
|
|
699
953
|
buildToFilter,
|
|
700
954
|
classifyEmailFilter,
|
|
955
|
+
filterToClientSide,
|
|
956
|
+
filterQueryClientSide,
|
|
701
957
|
};
|
package/email/send.js
CHANGED
|
@@ -138,7 +138,7 @@ async function handleSendEmail(args) {
|
|
|
138
138
|
content: [
|
|
139
139
|
{
|
|
140
140
|
type: 'text',
|
|
141
|
-
text: tipsText
|
|
141
|
+
text: `${tipsText}\n\n---\n\n${preview.content[0].text}`,
|
|
142
142
|
},
|
|
143
143
|
],
|
|
144
144
|
_meta: { mailTips: tipsResult._meta },
|
package/folder/list.js
CHANGED
|
@@ -272,7 +272,7 @@ function formatFolderHierarchy(folders, includeItemCounts) {
|
|
|
272
272
|
// Add children
|
|
273
273
|
const childLines = folder.children
|
|
274
274
|
.map((childId) => formatSubtree(childId, level + 1))
|
|
275
|
-
.filter((
|
|
275
|
+
.filter((childLine) => childLine.length > 0)
|
|
276
276
|
.join('\n');
|
|
277
277
|
|
|
278
278
|
return childLines.length > 0 ? `${line}\n${childLines}` : line;
|
package/llms.txt
CHANGED
|
@@ -33,6 +33,7 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
33
33
|
|
|
34
34
|
- **MCP safety annotations** on all 22 tools — AI clients auto-approve reads and prompt for destructive operations
|
|
35
35
|
- **Send-email protections**: pre-send mail tips, dry-run preview, session rate limiting, recipient allowlist
|
|
36
|
+
- **Rule protections**: dry-run preview on create/update, rate limiting, recipient allowlist on forward/redirect, no permanent-delete action
|
|
36
37
|
- **Token-optimised**: 22 tools instead of 55 saves ~11,000 tokens per turn (~64% reduction), improving AI accuracy and context efficiency
|
|
37
38
|
- These safeguards reduce risk but are not foolproof — always review actions before approving
|
|
38
39
|
|
|
@@ -62,7 +63,7 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
|
|
|
62
63
|
- **Calendar (3 tools)**: `list-events`, `create-event`, `manage-event`
|
|
63
64
|
- **Contacts (2 tools)**: `manage-contact`, `search-people`
|
|
64
65
|
- **Folders (1 tool)**: `folders` — list, create, move, stats
|
|
65
|
-
- **Rules (1 tool)**: `manage-rules` — list, create, reorder
|
|
66
|
+
- **Rules (1 tool)**: `manage-rules` — list, create, update, reorder, delete
|
|
66
67
|
- **Categories (3 tools)**: `manage-category`, `apply-category`, `manage-focused-inbox`
|
|
67
68
|
- **Settings (1 tool)**: `mailbox-settings` — get, set auto-replies, set working hours
|
|
68
69
|
- **Advanced (2 tools)**: `access-shared-mailbox`, `find-meeting-rooms`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebearapps/outlook-assistant",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.1",
|
|
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",
|