@littlebearapps/outlook-assistant 3.12.1 → 3.14.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/.env.example +27 -3
- package/README.md +108 -33
- package/advanced/index.js +44 -174
- package/auth/auth-errors.js +23 -1
- package/auth/client-config.js +142 -0
- package/auth/index.js +4 -2
- package/auth/oauth-server.js +12 -2
- package/auth/token-manager.js +7 -3
- package/auth/token-storage.js +46 -33
- package/auth/tools.js +223 -93
- package/calendar/attendees.js +36 -0
- package/calendar/cancel.js +9 -25
- package/calendar/create.js +42 -48
- package/calendar/decline.js +10 -25
- package/calendar/delete.js +10 -25
- package/calendar/index.js +20 -37
- package/calendar/list.js +4 -16
- package/calendar/preview.js +335 -0
- package/calendar/update.js +42 -86
- package/categories/index.js +59 -264
- package/config.js +36 -2
- package/contacts/index.js +72 -128
- package/email/attachments.js +42 -124
- package/email/conversations.js +44 -78
- package/email/delta.js +10 -34
- package/email/draft.js +140 -96
- package/email/export.js +141 -110
- package/email/folder-utils.js +3 -2
- package/email/headers.js +11 -49
- package/email/index.js +85 -109
- package/email/list.js +4 -17
- package/email/mail-tips.js +86 -57
- package/email/mark-as-read.js +13 -49
- package/email/mime.js +14 -49
- package/email/read.js +16 -50
- package/email/search.js +46 -86
- package/email/send.js +82 -48
- package/folder/create.js +6 -25
- package/folder/delete.js +117 -38
- package/folder/index.js +17 -16
- package/folder/list.js +5 -17
- package/folder/move.js +13 -42
- package/folder/resolve.js +11 -6
- package/folder/stats.js +6 -20
- package/index.js +23 -45
- package/llms-install.md +31 -7
- package/llms.txt +19 -10
- package/outlook-auth-server.js +10 -3
- package/package.json +6 -2
- package/request-handler.js +217 -116
- package/rules/create.js +27 -70
- package/rules/index.js +30 -92
- package/rules/list.js +5 -17
- package/rules/rule-builder.js +57 -20
- package/rules/update.js +26 -60
- package/server.js +37 -0
- package/settings/index.js +142 -143
- package/tools.js +30 -0
- package/utils/field-presets.js +4 -2
- package/utils/graph-api.js +65 -22
- package/utils/logger.js +251 -0
- package/utils/mock-data.js +91 -2
- package/utils/read-only.js +59 -0
- package/utils/response-formatter.js +54 -15
- package/utils/risk-classes.js +324 -0
- package/utils/safe-write.js +372 -6
- package/utils/safety.js +109 -25
- package/utils/server-instructions.js +62 -0
- package/utils/tool-error.js +33 -0
package/server.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server factory: builds the SDK Server for the tool registry.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from index.js (CLI flags, startup warnings, stdio transport) so
|
|
5
|
+
* protocol behaviour can be tested over an in-memory transport.
|
|
6
|
+
*/
|
|
7
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
8
|
+
const config = require('./config');
|
|
9
|
+
const { createRequestHandler } = require('./request-handler');
|
|
10
|
+
const { TOOLS } = require('./tools');
|
|
11
|
+
const { serverInstructions } = require('./utils/server-instructions');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {Array<object>} [tools] - tool definitions (default: the registry)
|
|
15
|
+
* @returns {Server}
|
|
16
|
+
*/
|
|
17
|
+
function createServer(tools = TOOLS) {
|
|
18
|
+
const server = new Server(
|
|
19
|
+
{ name: config.SERVER_NAME, version: config.SERVER_VERSION },
|
|
20
|
+
{
|
|
21
|
+
// Spec shape: the tool list never changes at runtime. Nothing else
|
|
22
|
+
// (resources, prompts, logging) is declared, so those methods return
|
|
23
|
+
// -32601 rather than empty stubs. (#276)
|
|
24
|
+
capabilities: { tools: { listChanged: false } },
|
|
25
|
+
// Model-facing safety rules and usage tips, sent in the initialize
|
|
26
|
+
// result (#271).
|
|
27
|
+
instructions: serverInstructions({ readOnly: config.READ_ONLY }),
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Handle all requests. Dispatch + error-shaping logic lives in
|
|
32
|
+
// request-handler.js so it is unit-testable without starting the transport.
|
|
33
|
+
server.fallbackRequestHandler = createRequestHandler(tools);
|
|
34
|
+
return server;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { createServer };
|
package/settings/index.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
8
8
|
const { ensureAuthenticated } = require('../auth');
|
|
9
|
+
const { toolMetadata } = require('../utils/risk-classes');
|
|
10
|
+
const { toolError, authRequiredError } = require('../utils/tool-error');
|
|
11
|
+
const { dryRunResult, dryRunUnsupported } = require('../utils/safety');
|
|
9
12
|
|
|
10
13
|
// Days of the week for working hours
|
|
11
14
|
const DAYS_OF_WEEK = [
|
|
@@ -91,6 +94,88 @@ function formatAutomaticReplies(settings) {
|
|
|
91
94
|
return lines.join('\n');
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
/** Who gets the external reply, for each externalAudience value. */
|
|
98
|
+
const EXTERNAL_AUDIENCE_LABELS = {
|
|
99
|
+
none: 'nobody',
|
|
100
|
+
contactsOnly: 'only senders in your contacts',
|
|
101
|
+
all: 'all external senders',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Longest stretch of a reply message quoted in a preview. */
|
|
105
|
+
const REPLY_PREVIEW_CHARS = 100;
|
|
106
|
+
|
|
107
|
+
/** "get a 25-character reply: "…"" (or no message) for a preview. */
|
|
108
|
+
function describeReply(message, unchanged) {
|
|
109
|
+
const tag = unchanged ? ' (unchanged)' : '';
|
|
110
|
+
if (!message) return `— no reply message is set${tag}`;
|
|
111
|
+
const quoted =
|
|
112
|
+
message.length > REPLY_PREVIEW_CHARS
|
|
113
|
+
? `${message.substring(0, REPLY_PREVIEW_CHARS)}…`
|
|
114
|
+
: message;
|
|
115
|
+
return `get a ${message.length}-character reply${tag}: "${quoted}"`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** "scheduled, from A to B (UTC)" */
|
|
119
|
+
function describeSchedule(start, end) {
|
|
120
|
+
if (!start?.dateTime || !end?.dateTime) return 'scheduled';
|
|
121
|
+
return start.timeZone === end.timeZone
|
|
122
|
+
? `scheduled, from ${start.dateTime} to ${end.dateTime} (${start.timeZone})`
|
|
123
|
+
: `scheduled, from ${start.dateTime} (${start.timeZone}) to ${end.dateTime} (${end.timeZone})`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* dryRun preview for set-auto-replies (#274): who would get an automatic
|
|
128
|
+
* reply, when, and how long each message is. Fields the call doesn't set
|
|
129
|
+
* keep their current values, so those are read first and marked unchanged.
|
|
130
|
+
* @param {object} changes - The automaticRepliesSetting the call would PATCH
|
|
131
|
+
* @param {object} current - The current automaticRepliesSetting
|
|
132
|
+
*/
|
|
133
|
+
function previewAutomaticReplies(changes, current) {
|
|
134
|
+
const effective = { ...current, ...changes };
|
|
135
|
+
const unchanged = (field) => !(field in changes);
|
|
136
|
+
const lines = [];
|
|
137
|
+
|
|
138
|
+
const status = effective.status;
|
|
139
|
+
const statusTag = unchanged('status') ? ' (unchanged)' : '';
|
|
140
|
+
if (status === 'disabled') {
|
|
141
|
+
lines.push(
|
|
142
|
+
`Status: off (disabled)${statusTag}. Nobody gets an automatic reply.`
|
|
143
|
+
);
|
|
144
|
+
return dryRunResult(lines, { settings: effective });
|
|
145
|
+
}
|
|
146
|
+
if (status === 'alwaysEnabled') {
|
|
147
|
+
lines.push(`Status: on now, with no end date (alwaysEnabled)${statusTag}.`);
|
|
148
|
+
} else {
|
|
149
|
+
lines.push(
|
|
150
|
+
`Status: ${describeSchedule(effective.scheduledStartDateTime, effective.scheduledEndDateTime)}${statusTag}.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
lines.push(
|
|
155
|
+
`Internal senders (your organisation): ${describeReply(effective.internalReplyMessage, unchanged('internalReplyMessage'))}`
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const audience = effective.externalAudience;
|
|
159
|
+
const audienceTag = `externalAudience=${audience || 'unknown'}${unchanged('externalAudience') ? '; unchanged' : ''}`;
|
|
160
|
+
if (audience === 'none') {
|
|
161
|
+
lines.push(`External senders: nobody (${audienceTag}).`);
|
|
162
|
+
} else {
|
|
163
|
+
const who = EXTERNAL_AUDIENCE_LABELS[audience] || 'unknown audience';
|
|
164
|
+
lines.push(
|
|
165
|
+
`External senders: ${who} (${audienceTag}) ${describeReply(effective.externalReplyMessage, unchanged('externalReplyMessage'))}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (changes.status === 'alwaysEnabled') {
|
|
170
|
+
lines.push(
|
|
171
|
+
'',
|
|
172
|
+
'Note: Personal Outlook.com accounts only support scheduled replies, and Graph may leave them off. Pass `startDateTime` + `endDateTime` there instead.'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return dryRunResult(lines, { settings: effective });
|
|
177
|
+
}
|
|
178
|
+
|
|
94
179
|
/**
|
|
95
180
|
* Get mailbox settings handler
|
|
96
181
|
*/
|
|
@@ -189,23 +274,9 @@ async function handleGetMailboxSettings(args) {
|
|
|
189
274
|
};
|
|
190
275
|
} catch (error) {
|
|
191
276
|
if (error.message === 'Authentication required') {
|
|
192
|
-
return
|
|
193
|
-
content: [
|
|
194
|
-
{
|
|
195
|
-
type: 'text',
|
|
196
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
197
|
-
},
|
|
198
|
-
],
|
|
199
|
-
};
|
|
277
|
+
return authRequiredError();
|
|
200
278
|
}
|
|
201
|
-
return {
|
|
202
|
-
content: [
|
|
203
|
-
{
|
|
204
|
-
type: 'text',
|
|
205
|
-
text: `Error getting mailbox settings: ${error.message}`,
|
|
206
|
-
},
|
|
207
|
-
],
|
|
208
|
-
};
|
|
279
|
+
return toolError(`Error getting mailbox settings: ${error.message}`);
|
|
209
280
|
}
|
|
210
281
|
}
|
|
211
282
|
|
|
@@ -220,6 +291,7 @@ async function handleSetAutomaticReplies(args) {
|
|
|
220
291
|
internalReplyMessage,
|
|
221
292
|
externalReplyMessage,
|
|
222
293
|
externalAudience,
|
|
294
|
+
dryRun = false,
|
|
223
295
|
} = args;
|
|
224
296
|
|
|
225
297
|
try {
|
|
@@ -273,14 +345,9 @@ async function handleSetAutomaticReplies(args) {
|
|
|
273
345
|
// External audience
|
|
274
346
|
if (externalAudience) {
|
|
275
347
|
if (!['none', 'contactsOnly', 'all'].includes(externalAudience)) {
|
|
276
|
-
return
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
type: 'text',
|
|
280
|
-
text: "externalAudience must be 'none', 'contactsOnly', or 'all'.",
|
|
281
|
-
},
|
|
282
|
-
],
|
|
283
|
-
};
|
|
348
|
+
return toolError(
|
|
349
|
+
"externalAudience must be 'none', 'contactsOnly', or 'all'."
|
|
350
|
+
);
|
|
284
351
|
}
|
|
285
352
|
settings.externalAudience = externalAudience;
|
|
286
353
|
}
|
|
@@ -290,14 +357,20 @@ async function handleSetAutomaticReplies(args) {
|
|
|
290
357
|
// externalAudience without enabled/scheduled — previously the wrapper
|
|
291
358
|
// announced "Automatic replies updated!" with no actual state change.
|
|
292
359
|
if (Object.keys(settings).length === 0) {
|
|
293
|
-
return
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
360
|
+
return toolError(
|
|
361
|
+
'No automatic-reply settings were provided. To change state, pass `enabled: true|false` or `startDateTime` + `endDateTime`. To update messages or audience, pass `internalReplyMessage`, `externalReplyMessage`, or `externalAudience`.'
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// dryRun: read the current setting to fill in what this call leaves
|
|
366
|
+
// alone, and say who would get a reply; change nothing.
|
|
367
|
+
if (dryRun) {
|
|
368
|
+
const current = await callGraphAPI(
|
|
369
|
+
accessToken,
|
|
370
|
+
'GET',
|
|
371
|
+
'me/mailboxSettings/automaticRepliesSetting'
|
|
372
|
+
);
|
|
373
|
+
return previewAutomaticReplies(settings, current || {});
|
|
301
374
|
}
|
|
302
375
|
|
|
303
376
|
// Apply settings
|
|
@@ -377,23 +450,9 @@ async function handleSetAutomaticReplies(args) {
|
|
|
377
450
|
};
|
|
378
451
|
} catch (error) {
|
|
379
452
|
if (error.message === 'Authentication required') {
|
|
380
|
-
return
|
|
381
|
-
content: [
|
|
382
|
-
{
|
|
383
|
-
type: 'text',
|
|
384
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
385
|
-
},
|
|
386
|
-
],
|
|
387
|
-
};
|
|
453
|
+
return authRequiredError();
|
|
388
454
|
}
|
|
389
|
-
return {
|
|
390
|
-
content: [
|
|
391
|
-
{
|
|
392
|
-
type: 'text',
|
|
393
|
-
text: `Error setting automatic replies: ${error.message}`,
|
|
394
|
-
},
|
|
395
|
-
],
|
|
396
|
-
};
|
|
455
|
+
return toolError(`Error setting automatic replies: ${error.message}`);
|
|
397
456
|
}
|
|
398
457
|
}
|
|
399
458
|
|
|
@@ -405,37 +464,22 @@ async function handleSetWorkingHours(args) {
|
|
|
405
464
|
|
|
406
465
|
// Validate inputs
|
|
407
466
|
if (!startTime && !endTime && !daysOfWeek && !timeZone) {
|
|
408
|
-
return
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
type: 'text',
|
|
412
|
-
text: 'At least one of startTime, endTime, daysOfWeek, or timeZone is required.',
|
|
413
|
-
},
|
|
414
|
-
],
|
|
415
|
-
};
|
|
467
|
+
return toolError(
|
|
468
|
+
'At least one of startTime, endTime, daysOfWeek, or timeZone is required.'
|
|
469
|
+
);
|
|
416
470
|
}
|
|
417
471
|
|
|
418
472
|
// Validate time format (HH:MM or HH:MM:SS)
|
|
419
473
|
const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/;
|
|
420
474
|
if (startTime && !timeRegex.test(startTime)) {
|
|
421
|
-
return
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
type: 'text',
|
|
425
|
-
text: "startTime must be in HH:MM or HH:MM:SS format (e.g., '09:00' or '09:00:00').",
|
|
426
|
-
},
|
|
427
|
-
],
|
|
428
|
-
};
|
|
475
|
+
return toolError(
|
|
476
|
+
"startTime must be in HH:MM or HH:MM:SS format (e.g., '09:00' or '09:00:00')."
|
|
477
|
+
);
|
|
429
478
|
}
|
|
430
479
|
if (endTime && !timeRegex.test(endTime)) {
|
|
431
|
-
return
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
type: 'text',
|
|
435
|
-
text: "endTime must be in HH:MM or HH:MM:SS format (e.g., '17:00' or '17:00:00').",
|
|
436
|
-
},
|
|
437
|
-
],
|
|
438
|
-
};
|
|
480
|
+
return toolError(
|
|
481
|
+
"endTime must be in HH:MM or HH:MM:SS format (e.g., '17:00' or '17:00:00')."
|
|
482
|
+
);
|
|
439
483
|
}
|
|
440
484
|
|
|
441
485
|
// Validate days of week
|
|
@@ -444,14 +488,9 @@ async function handleSetWorkingHours(args) {
|
|
|
444
488
|
(d) => !DAYS_OF_WEEK.includes(d.toLowerCase())
|
|
445
489
|
);
|
|
446
490
|
if (invalidDays.length > 0) {
|
|
447
|
-
return
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
type: 'text',
|
|
451
|
-
text: `Invalid days: ${invalidDays.join(', ')}. Valid days: ${DAYS_OF_WEEK.join(', ')}`,
|
|
452
|
-
},
|
|
453
|
-
],
|
|
454
|
-
};
|
|
491
|
+
return toolError(
|
|
492
|
+
`Invalid days: ${invalidDays.join(', ')}. Valid days: ${DAYS_OF_WEEK.join(', ')}`
|
|
493
|
+
);
|
|
455
494
|
}
|
|
456
495
|
}
|
|
457
496
|
|
|
@@ -506,23 +545,9 @@ async function handleSetWorkingHours(args) {
|
|
|
506
545
|
};
|
|
507
546
|
} catch (error) {
|
|
508
547
|
if (error.message === 'Authentication required') {
|
|
509
|
-
return
|
|
510
|
-
content: [
|
|
511
|
-
{
|
|
512
|
-
type: 'text',
|
|
513
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
514
|
-
},
|
|
515
|
-
],
|
|
516
|
-
};
|
|
548
|
+
return authRequiredError();
|
|
517
549
|
}
|
|
518
|
-
return {
|
|
519
|
-
content: [
|
|
520
|
-
{
|
|
521
|
-
type: 'text',
|
|
522
|
-
text: `Error setting working hours: ${error.message}`,
|
|
523
|
-
},
|
|
524
|
-
],
|
|
525
|
-
};
|
|
550
|
+
return toolError(`Error setting working hours: ${error.message}`);
|
|
526
551
|
}
|
|
527
552
|
}
|
|
528
553
|
|
|
@@ -548,23 +573,9 @@ async function handleGetAutomaticReplies() {
|
|
|
548
573
|
};
|
|
549
574
|
} catch (error) {
|
|
550
575
|
if (error.message === 'Authentication required') {
|
|
551
|
-
return
|
|
552
|
-
content: [
|
|
553
|
-
{
|
|
554
|
-
type: 'text',
|
|
555
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
556
|
-
},
|
|
557
|
-
],
|
|
558
|
-
};
|
|
576
|
+
return authRequiredError();
|
|
559
577
|
}
|
|
560
|
-
return {
|
|
561
|
-
content: [
|
|
562
|
-
{
|
|
563
|
-
type: 'text',
|
|
564
|
-
text: `Error getting automatic replies: ${error.message}`,
|
|
565
|
-
},
|
|
566
|
-
],
|
|
567
|
-
};
|
|
578
|
+
return toolError(`Error getting automatic replies: ${error.message}`);
|
|
568
579
|
}
|
|
569
580
|
}
|
|
570
581
|
|
|
@@ -590,23 +601,9 @@ async function handleGetWorkingHours() {
|
|
|
590
601
|
};
|
|
591
602
|
} catch (error) {
|
|
592
603
|
if (error.message === 'Authentication required') {
|
|
593
|
-
return
|
|
594
|
-
content: [
|
|
595
|
-
{
|
|
596
|
-
type: 'text',
|
|
597
|
-
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
598
|
-
},
|
|
599
|
-
],
|
|
600
|
-
};
|
|
604
|
+
return authRequiredError();
|
|
601
605
|
}
|
|
602
|
-
return {
|
|
603
|
-
content: [
|
|
604
|
-
{
|
|
605
|
-
type: 'text',
|
|
606
|
-
text: `Error getting working hours: ${error.message}`,
|
|
607
|
-
},
|
|
608
|
-
],
|
|
609
|
-
};
|
|
606
|
+
return toolError(`Error getting working hours: ${error.message}`);
|
|
610
607
|
}
|
|
611
608
|
}
|
|
612
609
|
|
|
@@ -615,13 +612,8 @@ const settingsTools = [
|
|
|
615
612
|
{
|
|
616
613
|
name: 'mailbox-settings',
|
|
617
614
|
description:
|
|
618
|
-
'Read or update mailbox-level settings (idempotent — safe to retry; sets are PATCH-style and merge with existing state). action=`get` (default) returns settings — use `section` to filter (`language`, `timeZone`, `workingHours`, `automaticRepliesSetting`, or `all`). action=`set-auto-replies` configures out-of-office: `enabled` true/false, optional `startDateTime`/`endDateTime` (ISO 8601) for scheduled mode, `internalReplyMessage` and (optionally) `externalReplyMessage
|
|
619
|
-
|
|
620
|
-
title: 'Mailbox Settings',
|
|
621
|
-
readOnlyHint: false,
|
|
622
|
-
destructiveHint: false,
|
|
623
|
-
idempotentHint: true,
|
|
624
|
-
},
|
|
615
|
+
'Read or update mailbox-level settings (idempotent — safe to retry; sets are PATCH-style and merge with existing state). action=`get` (default) returns settings — use `section` to filter (`language`, `timeZone`, `workingHours`, `automaticRepliesSetting`, or `all`). action=`set-auto-replies` configures out-of-office: `enabled` true/false, optional `startDateTime`/`endDateTime` (ISO 8601) for scheduled mode, `internalReplyMessage` and (optionally) `externalReplyMessage` and `externalAudience` (none/contactsOnly/all); pass `dryRun: true` to preview who would get replies without changing anything. action=`set-working-hours` updates the schedule: `startTime`/`endTime` (HH:MM) and `daysOfWeek` (array of `monday`..`sunday`). Returns the updated settings object on set actions.',
|
|
616
|
+
...toolMetadata('mailbox-settings', 'Mailbox Settings'),
|
|
625
617
|
inputSchema: {
|
|
626
618
|
type: 'object',
|
|
627
619
|
properties: {
|
|
@@ -674,6 +666,11 @@ const settingsTools = [
|
|
|
674
666
|
enum: ['none', 'contactsOnly', 'all'],
|
|
675
667
|
description: 'Who receives external reply (action=set-auto-replies)',
|
|
676
668
|
},
|
|
669
|
+
dryRun: {
|
|
670
|
+
type: 'boolean',
|
|
671
|
+
description:
|
|
672
|
+
'Preview only (action=set-auto-replies): nothing is changed. Shows who would get automatic replies, the schedule and each message length. Other actions refuse dryRun and change nothing. Default false.',
|
|
673
|
+
},
|
|
677
674
|
// set-working-hours params
|
|
678
675
|
startTime: {
|
|
679
676
|
type: 'string',
|
|
@@ -705,6 +702,13 @@ const settingsTools = [
|
|
|
705
702
|
},
|
|
706
703
|
handler: async (args) => {
|
|
707
704
|
const action = args.action || 'get';
|
|
705
|
+
if (args.dryRun && action !== 'set-auto-replies') {
|
|
706
|
+
return dryRunUnsupported(
|
|
707
|
+
'mailbox-settings',
|
|
708
|
+
action,
|
|
709
|
+
'set-auto-replies'
|
|
710
|
+
);
|
|
711
|
+
}
|
|
708
712
|
switch (action) {
|
|
709
713
|
case 'set-auto-replies':
|
|
710
714
|
return handleSetAutomaticReplies(args);
|
|
@@ -713,14 +717,9 @@ const settingsTools = [
|
|
|
713
717
|
case 'get':
|
|
714
718
|
return handleGetMailboxSettings(args);
|
|
715
719
|
default:
|
|
716
|
-
return
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
type: 'text',
|
|
720
|
-
text: `Unknown action '${action}'. Valid actions: get, set-auto-replies, set-working-hours.`,
|
|
721
|
-
},
|
|
722
|
-
],
|
|
723
|
-
};
|
|
720
|
+
return toolError(
|
|
721
|
+
`Unknown action '${action}'. Valid actions: get, set-auto-replies, set-working-hours.`
|
|
722
|
+
);
|
|
724
723
|
}
|
|
725
724
|
},
|
|
726
725
|
},
|
package/tools.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry: every tool the server exposes, in listing order.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from index.js so tests can load the real tool list without
|
|
5
|
+
* starting the stdio transport. Add a new module's tools here (and classify
|
|
6
|
+
* them in utils/risk-classes.js).
|
|
7
|
+
*/
|
|
8
|
+
const { authTools } = require('./auth');
|
|
9
|
+
const { calendarTools } = require('./calendar');
|
|
10
|
+
const { emailTools } = require('./email');
|
|
11
|
+
const { folderTools } = require('./folder');
|
|
12
|
+
const { rulesTools } = require('./rules');
|
|
13
|
+
const { contactsTools } = require('./contacts');
|
|
14
|
+
const { categoriesTools } = require('./categories');
|
|
15
|
+
const { settingsTools } = require('./settings');
|
|
16
|
+
const { advancedTools } = require('./advanced');
|
|
17
|
+
|
|
18
|
+
const TOOLS = [
|
|
19
|
+
...authTools,
|
|
20
|
+
...calendarTools,
|
|
21
|
+
...emailTools,
|
|
22
|
+
...folderTools,
|
|
23
|
+
...rulesTools,
|
|
24
|
+
...contactsTools,
|
|
25
|
+
...categoriesTools,
|
|
26
|
+
...settingsTools,
|
|
27
|
+
...advancedTools,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
module.exports = { TOOLS };
|
package/utils/field-presets.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* and reduce response size/token usage.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
const { log } = require('./logger');
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Field presets for different use cases
|
|
10
12
|
*/
|
|
@@ -255,7 +257,7 @@ const FOLDER_FIELDS = {
|
|
|
255
257
|
function getEmailFields(preset = 'list') {
|
|
256
258
|
const fields = FIELD_PRESETS[preset];
|
|
257
259
|
if (!fields) {
|
|
258
|
-
|
|
260
|
+
log.debug(`Unknown preset: ${preset}, falling back to 'list'`);
|
|
259
261
|
return FIELD_PRESETS.list.join(',');
|
|
260
262
|
}
|
|
261
263
|
return fields.join(',');
|
|
@@ -269,7 +271,7 @@ function getEmailFields(preset = 'list') {
|
|
|
269
271
|
function getFolderFields(preset = 'basic') {
|
|
270
272
|
const fields = FOLDER_FIELDS[preset];
|
|
271
273
|
if (!fields) {
|
|
272
|
-
|
|
274
|
+
log.debug(`Unknown folder preset: ${preset}, falling back to 'basic'`);
|
|
273
275
|
return FOLDER_FIELDS.basic.join(',');
|
|
274
276
|
}
|
|
275
277
|
return fields.join(',');
|
package/utils/graph-api.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const https = require('https');
|
|
5
5
|
const config = require('../config');
|
|
6
6
|
const mockData = require('./mock-data');
|
|
7
|
+
const { log, graphPathShape } = require('./logger');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Guard for caller-supplied full URLs (nextLink/deltaLink continuations).
|
|
@@ -270,7 +271,7 @@ function sleep(ms) {
|
|
|
270
271
|
* final response (2xx, or the last non-retried error status)
|
|
271
272
|
* @throws {Error} The network error of the final attempt
|
|
272
273
|
*/
|
|
273
|
-
async function
|
|
274
|
+
async function sendWithRetry(request) {
|
|
274
275
|
const method = String(request.method).toUpperCase();
|
|
275
276
|
const timeoutMs = request.timeoutMs || config.REQUEST_TIMEOUT_MS;
|
|
276
277
|
let networkRetryUsed = false;
|
|
@@ -316,7 +317,8 @@ async function requestWithRetry(request) {
|
|
|
316
317
|
}
|
|
317
318
|
}
|
|
318
319
|
|
|
319
|
-
|
|
320
|
+
log.increment('graphRetries');
|
|
321
|
+
log.debug(
|
|
320
322
|
`[GRAPH-API] ${method} ${networkError ? networkError.code : response.status}; ` +
|
|
321
323
|
`retry ${retry + 1}/${MAX_RETRIES} in ${delayMs} ms`
|
|
322
324
|
);
|
|
@@ -325,6 +327,36 @@ async function requestWithRetry(request) {
|
|
|
325
327
|
}
|
|
326
328
|
}
|
|
327
329
|
|
|
330
|
+
/**
|
|
331
|
+
* sendWithRetry, noting a final failure on the current tool call's log line
|
|
332
|
+
* as status (or network error code), method and a PII-free path shape (#278).
|
|
333
|
+
* @param {object} request - See sendWithRetry
|
|
334
|
+
* @returns {Promise<{status: number, headers: object, text: string}>}
|
|
335
|
+
*/
|
|
336
|
+
async function requestWithRetry(request) {
|
|
337
|
+
const method = String(request.method).toUpperCase();
|
|
338
|
+
try {
|
|
339
|
+
const response = await sendWithRetry(request);
|
|
340
|
+
if (response.status >= 400) {
|
|
341
|
+
log.note(
|
|
342
|
+
'graph',
|
|
343
|
+
`${response.status} ${method} ${graphPathShape(request.url)}`
|
|
344
|
+
);
|
|
345
|
+
log.debug(
|
|
346
|
+
`[GRAPH-API] ${method} ${request.url} failed with ${response.status}: ${response.text}`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return response;
|
|
350
|
+
} catch (error) {
|
|
351
|
+
log.note(
|
|
352
|
+
'graph',
|
|
353
|
+
`${error.code || 'network-error'} ${method} ${graphPathShape(request.url)}`
|
|
354
|
+
);
|
|
355
|
+
log.debug(`[GRAPH-API] ${method} ${request.url} failed:`, error);
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
328
360
|
/**
|
|
329
361
|
* Makes a request to the Microsoft Graph API
|
|
330
362
|
* In test mode (USE_TEST_MODE=true), routes to mock data instead of the real API.
|
|
@@ -357,7 +389,7 @@ async function callGraphAPI(
|
|
|
357
389
|
try {
|
|
358
390
|
finalUrl = buildGraphUrl(path, queryParams);
|
|
359
391
|
} catch (error) {
|
|
360
|
-
|
|
392
|
+
log.debug('Error calling Graph API:', error);
|
|
361
393
|
throw error;
|
|
362
394
|
}
|
|
363
395
|
|
|
@@ -422,12 +454,18 @@ async function callGraphAPI(
|
|
|
422
454
|
|
|
423
455
|
/**
|
|
424
456
|
* Calls Graph API with pagination support to retrieve all results up to maxCount
|
|
457
|
+
*
|
|
458
|
+
* Reports truncation (#279): `hasMore` is true when it stopped with more
|
|
459
|
+
* available (at maxCount with items trimmed or a further page, or because
|
|
460
|
+
* Graph repeated a nextLink). `@odata.count` is set only when every item was
|
|
461
|
+
* returned, so a page size is never presented as a total. `@odata.nextLink`
|
|
462
|
+
* is kept only when it continues exactly after the returned items.
|
|
425
463
|
* @param {string} accessToken - The access token for authentication
|
|
426
464
|
* @param {string} method - HTTP method (GET only for pagination)
|
|
427
465
|
* @param {string} path - API endpoint path
|
|
428
466
|
* @param {object} queryParams - Initial query parameters
|
|
429
467
|
* @param {number} maxCount - Maximum number of items to retrieve (0 = all)
|
|
430
|
-
* @returns {Promise<object
|
|
468
|
+
* @returns {Promise<{value: Array<object>, hasMore: boolean, '@odata.count'?: number, '@odata.nextLink'?: string}>}
|
|
431
469
|
* @throws {Error} If method is not 'GET'
|
|
432
470
|
* @throws {Error} If any page request fails for any other reason
|
|
433
471
|
*/
|
|
@@ -443,13 +481,14 @@ async function callGraphAPIPaginated(
|
|
|
443
481
|
}
|
|
444
482
|
|
|
445
483
|
const allItems = [];
|
|
446
|
-
|
|
484
|
+
const seenLinks = new Set();
|
|
447
485
|
let currentUrl = path;
|
|
448
486
|
let currentParams = { ...queryParams };
|
|
487
|
+
let nextLink;
|
|
488
|
+
let hasMore = false;
|
|
449
489
|
|
|
450
490
|
try {
|
|
451
|
-
|
|
452
|
-
// Make API call
|
|
491
|
+
for (;;) {
|
|
453
492
|
const response = await callGraphAPI(
|
|
454
493
|
accessToken,
|
|
455
494
|
method,
|
|
@@ -458,35 +497,39 @@ async function callGraphAPIPaginated(
|
|
|
458
497
|
currentParams
|
|
459
498
|
);
|
|
460
499
|
|
|
461
|
-
// Add items from this page
|
|
462
500
|
if (response.value && Array.isArray(response.value)) {
|
|
463
501
|
allItems.push(...response.value);
|
|
464
502
|
}
|
|
503
|
+
nextLink = response['@odata.nextLink'];
|
|
465
504
|
|
|
466
|
-
//
|
|
505
|
+
// Stop at the desired count; more remain if items were trimmed or
|
|
506
|
+
// there is another page.
|
|
467
507
|
if (maxCount > 0 && allItems.length >= maxCount) {
|
|
508
|
+
hasMore = allItems.length > maxCount || Boolean(nextLink);
|
|
509
|
+
if (allItems.length > maxCount) nextLink = undefined;
|
|
468
510
|
break;
|
|
469
511
|
}
|
|
470
|
-
|
|
471
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
currentUrl = nextLink;
|
|
477
|
-
currentParams = {}; // nextLink already contains all params
|
|
512
|
+
if (!nextLink) break;
|
|
513
|
+
// A repeated link would loop forever; stop and say there is more.
|
|
514
|
+
if (seenLinks.has(nextLink)) {
|
|
515
|
+
hasMore = true;
|
|
516
|
+
nextLink = undefined;
|
|
517
|
+
break;
|
|
478
518
|
}
|
|
479
|
-
|
|
519
|
+
seenLinks.add(nextLink);
|
|
520
|
+
currentUrl = nextLink; // the full nextLink URL carries every param
|
|
521
|
+
currentParams = {};
|
|
522
|
+
}
|
|
480
523
|
|
|
481
|
-
// Trim to exact count if needed
|
|
482
524
|
const finalItems = maxCount > 0 ? allItems.slice(0, maxCount) : allItems;
|
|
483
|
-
|
|
484
525
|
return {
|
|
485
526
|
value: finalItems,
|
|
486
|
-
|
|
527
|
+
hasMore,
|
|
528
|
+
...(!hasMore && { '@odata.count': finalItems.length }),
|
|
529
|
+
...(hasMore && nextLink && { '@odata.nextLink': nextLink }),
|
|
487
530
|
};
|
|
488
531
|
} catch (error) {
|
|
489
|
-
|
|
532
|
+
log.debug('Error during pagination:', error);
|
|
490
533
|
throw error;
|
|
491
534
|
}
|
|
492
535
|
}
|