@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 CHANGED
@@ -14,6 +14,7 @@
14
14
  <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>
15
15
  <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>
16
16
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
17
+ <a href="https://glama.ai/mcp/servers/littlebearapps/outlook-assistant"><img src="https://glama.ai/mcp/servers/littlebearapps/outlook-assistant/badges/score.svg" alt="Glama score" /></a>
17
18
  </p>
18
19
 
19
20
  Outlook Assistant connects AI assistants to your Microsoft Outlook account through the [Model Context Protocol](https://modelcontextprotocol.io/). Ask your AI assistant to search your inbox, send emails, schedule meetings, manage contacts, and configure mailbox settings — without leaving the conversation. Works with Claude, Cursor, Windsurf, and any MCP-compatible client.
@@ -36,8 +37,8 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
36
37
  - 🛡️ **Send emails with safety controls** — dry-run preview, pre-send mail tips (out-of-office, mailbox full, delivery restrictions), session rate limiting, and recipient allowlist to prevent mistakes
37
38
  - ✏️ **Draft emails for review** — create, update, and send drafts; reply and forward as drafts; preview before saving with dry-run mode
38
39
  - 📅 **Manage your calendar** — view upcoming events, schedule meetings with attendees, decline or cancel invitations
39
- - 📦 **Export emails** — save to Markdown, EML, MBOX, JSON, or HTML for archiving, analysis, or migration; export search results or entire threads in one call
40
- - 🔍 **Investigate email headers** — check DKIM, SPF, and DMARC authentication; trace delivery chains; analyse spam scores — useful for phishing investigation and compliance
40
+ - 📦 **Export emails** — save individual messages to Markdown, EML, JSON, or CSV; export full conversation threads to MBOX or HTML; bulk-export search results in one call
41
+ - 🔍 **Investigate email headers** — full raw header access (DKIM, SPF, DMARC, delivery chain, X-Mailer, X-Originating-IP) for phishing investigation and compliance review
41
42
  - 🗂️ **Organise your inbox** — create folders, set up inbox rules, colour-code with categories, manage Focused Inbox — all work together for complete inbox automation
42
43
  - 🔄 **Track inbox changes** — delta sync detects new, modified, and deleted emails since your last check, with tokens for incremental polling
43
44
  - 👥 **Manage contacts** — search your contact book and organisational directory, create and update contact records
@@ -75,16 +76,18 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
75
76
 
76
77
  ### Export Formats
77
78
 
78
- | Format | Extension | When to Use It |
79
- |--------|-----------|----------------|
80
- | `mime` / `eml` | `.eml` | Legal holds, forensic preservation, importing into other mail clients |
81
- | `mbox` | `.mbox` | Archiving entire conversation threads, migrating between systems |
82
- | `markdown` | `.md` | Pasting into documents, feeding into AI workflows |
83
- | `json` | `.json` | Data analysis, pipeline processing, compliance reporting |
84
- | `html` | `.html` | Visual archival with formatting intact |
85
- | `csv` | `.csv` | Spreadsheet import, bulk metadata analysis, compliance audits |
79
+ Format support varies by `target`:
86
80
 
87
- Export individual emails, search results, or entire conversation threads — use `target=messages` with a search query to batch-export without manually collecting IDs.
81
+ | Format | Extension | `target=message` (single) | `target=messages` (batch) | `target=conversation` (thread) |
82
+ |--------|-----------|--------|--------|--------|
83
+ | `mime` / `eml` | `.eml` | ✅ | – | ✅ |
84
+ | `mbox` | `.mbox` | – | – | ✅ |
85
+ | `markdown` | `.md` | ✅ | ✅ | ✅ |
86
+ | `json` | `.json` | ✅ | ✅ | ✅ |
87
+ | `html` | `.html` | – | – | ✅ |
88
+ | `csv` | `.csv` | ✅ | ✅ | ✅ |
89
+
90
+ Export individual emails, search results, or entire conversation threads — use `target=messages` with a search query (or the `query` shortcut) to batch-export without manually collecting IDs.
88
91
 
89
92
  ## Account Compatibility
90
93
 
@@ -100,7 +103,7 @@ Outlook Assistant works with both personal and work/school Microsoft accounts, b
100
103
  | Free-text `query` search | Limited — use `subject`, `from`, `to` filters instead | Full KQL support |
101
104
  | Categories | Full support | Full support |
102
105
  | Mailbox settings | Full support | Full support |
103
- | Focused Inbox | Not available | Full support |
106
+ | Focused Inbox | API works (overrides stored) but mail routing not affected | Full support |
104
107
  | Shared mailboxes | Not available | Requires `Mail.Read.Shared` |
105
108
  | Meeting room search | Not available | Requires `Place.Read.All` + admin consent |
106
109
 
@@ -109,7 +112,7 @@ Outlook Assistant works with both personal and work/school Microsoft accounts, b
109
112
  ### What Makes This Different
110
113
 
111
114
  - **Progressive search** — on accounts where Microsoft's `$search` API is limited, Outlook Assistant automatically falls back through up to 4 search strategies to find your emails. Most Graph API wrappers fail silently; this one adapts.
112
- - **Email forensics** — full header analysis (DKIM, SPF, DMARC, delivery chain, spam scores) built in as a first-class feature — useful for phishing investigation, compliance, and security review.
115
+ - **Email forensics** — raw header access for DKIM, SPF, DMARC, delivery chain, X-Mailer, X-Originating-IP, and spam scores. Returns the full data so you can investigate phishing, audit compliance, or trace delivery issues. (Auto-verdict is on the v3.8.0 roadmap; today the data is surfaced and analysed in-conversation.)
113
116
  - **Delta sync** — incremental inbox monitoring returns only what changed since your last check, with tokens for continuous polling. Designed for agent workflows that need to watch a mailbox.
114
117
  - **Batch operations** — flag, move, export, or categorise multiple emails in a single call. Search-driven export lets you batch-export results without collecting IDs manually.
115
118
  - **Pre-send intelligence** — check recipients for out-of-office, full mailbox, delivery restrictions, and moderation status before sending — no other Outlook MCP server offers this.
@@ -127,6 +130,17 @@ Outlook Assistant is designed with safety-first principles for AI-driven email a
127
130
  - **Session rate limiting** — configurable via `OUTLOOK_MAX_EMAILS_PER_SESSION` (default: unlimited)
128
131
  - **Recipient allowlist** — restrict sending to approved addresses/domains via `OUTLOOK_ALLOWED_RECIPIENTS`
129
132
 
133
+ > **Recommended setup**: enable both safety belts in your `.mcp.json` from day one. They're off by default; `auth action=about` reports their state and prints a setup hint when unset. See [`.mcp.json.example`](.mcp.json.example) for a copy-paste template.
134
+ >
135
+ > ```json
136
+ > "env": {
137
+ > "OUTLOOK_CLIENT_ID": "…",
138
+ > "OUTLOOK_CLIENT_SECRET": "…",
139
+ > "OUTLOOK_MAX_EMAILS_PER_SESSION": "10",
140
+ > "OUTLOOK_ALLOWED_RECIPIENTS": "your-domain.com,trusted@example.com"
141
+ > }
142
+ > ```
143
+
130
144
  **Draft protections** — The `draft` tool shares `send-email` safety controls: dry-run preview, recipient allowlist, mail-tips validation, and rate limiting. The `send` action shares the `send-email` rate limit counter, preventing circumvention via the draft-then-send pathway.
131
145
 
132
146
  **Token-optimised architecture** — Tools are consolidated using the STRAP (Single Tool, Resource, Action Pattern) approach. 22 tools instead of 55 reduces per-turn overhead by ~11,000 tokens (~64%), keeping more of the AI's context window available for your actual conversation. Fewer tools also means the AI selects the right tool more accurately — research shows tool selection degrades beyond ~40 tools.
@@ -356,6 +370,8 @@ No auth server needed. Works everywhere, including remote/headless environments.
356
370
  5. Tokens are saved to `~/.outlook-assistant-tokens.json` and **refresh automatically**
357
371
 
358
372
  > **Prerequisite**: Enable "Allow public client flows" in Azure Portal > your app > Authentication > Advanced settings.
373
+ >
374
+ > **Server restarts** (v3.7.2+): Device code state is persisted to `~/.outlook-assistant-pending-auth.json`, so `device-code-complete` works even if the MCP server restarts between steps 1 and 4 (e.g., Untether/Telegram bridge, Claude Desktop session changes).
359
375
 
360
376
  ### Browser Redirect Flow (Alternative)
361
377
 
@@ -431,6 +447,10 @@ If using browser flow: start the auth server first with `npm run auth-server`. I
431
447
 
432
448
  Enable "Allow public client flows" in Azure Portal > App registrations > Authentication > Advanced settings.
433
449
 
450
+ ### Token refresh fails after ~60 minutes (device code auth)
451
+
452
+ Fixed in v3.7.2. Earlier versions sent `client_secret` in token refresh requests for device-code auth, which Microsoft rejects for public client flows. Update to v3.7.2+ or re-authenticate.
453
+
434
454
  ### Empty API responses
435
455
 
436
456
  Check authentication status with the `auth` tool (action=status). Tokens may have expired — re-authenticate if needed.
@@ -467,7 +487,8 @@ USE_TEST_MODE=true npm start
467
487
  |-------|-------------|
468
488
  | [Getting Started](docs/how-to/getting-started/connect-outlook-to-claude.md) | Install, configure, and authenticate — start here |
469
489
  | [Azure Setup Guide](docs/guides/azure-setup.md) | Azure account creation, app registration, permissions, and secrets |
470
- | [How-To Guides](docs/how-to/index.md) | 28 practical guides for email, calendar, contacts, and settings |
490
+ | [How-To Guides](docs/how-to/index.md) | 29 practical guides for email, calendar, contacts, and settings |
491
+ | [Roadmap](ROADMAP.md) | Active milestones (v3.7.5, v3.8.0, v3.9.0) and recent releases |
471
492
  | [Troubleshooting & FAQ](docs/how-to/getting-started/verify-your-connection.md#common-connection-problems) | Common problems, re-authentication, and frequently asked questions |
472
493
  | [Tools Reference](docs/quickrefs/tools-reference.md) | All 22 tools with parameters |
473
494
  | [AI Agent Guide](docs/how-to/ai-agents/using-outlook-assistant-in-agents.md) | Tool selection and workflow patterns for AI agents |
@@ -494,10 +515,6 @@ For security concerns, please see our [Security Policy](SECURITY.md). Do not ope
494
515
 
495
516
  See [CHANGELOG.md](CHANGELOG.md) for version history.
496
517
 
497
- ## Listed On
498
-
499
- <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>
500
-
501
518
  ## About
502
519
 
503
520
  Built and maintained by [Little Bear Apps](https://littlebearapps.com). Outlook Assistant is open source under the [MIT License](LICENSE).
package/advanced/index.js CHANGED
@@ -46,7 +46,10 @@ function formatEmail(email, verbosity = 'standard') {
46
46
  * Requires Mail.Read.Shared permission
47
47
  */
48
48
  async function handleAccessSharedMailbox(args) {
49
- const { sharedMailbox, folder, count, outputVerbosity } = args;
49
+ // F-46: accept `email` as alias for `sharedMailbox`. The original
50
+ // param name is awkward; most callers reach for `email` first.
51
+ const { folder, count, outputVerbosity } = args;
52
+ const sharedMailbox = args.sharedMailbox || args.email;
50
53
 
51
54
  if (!sharedMailbox) {
52
55
  return {
@@ -458,11 +461,24 @@ async function handleFindMeetingRooms(args) {
458
461
  );
459
462
  rooms = roomsResponse.value || [];
460
463
  } catch (findRoomsError) {
464
+ // F-47: distinguish "feature not available on personal account"
465
+ // from generic permission errors. Personal Outlook.com accounts
466
+ // surface a 404 here; organizational accounts surface
467
+ // permission errors. Both look similar in Graph but mean very
468
+ // different things to the caller.
469
+ const errMsg = findRoomsError.message || '';
470
+ const isLikelyPersonal =
471
+ errMsg.includes('404') ||
472
+ errMsg.includes('Not Found') ||
473
+ errMsg.includes('NotFound');
474
+ const explanation = isLikelyPersonal
475
+ ? 'Meeting room search is M365-only. Personal Outlook.com accounts cannot use this feature — there are no rooms to find. Connect a Microsoft 365 work/school account to enable.'
476
+ : 'This feature requires:\n- Places.Read.All permission\n- Meeting rooms configured in your organization';
461
477
  return {
462
478
  content: [
463
479
  {
464
480
  type: 'text',
465
- text: `Unable to find meeting rooms.\n\n**Note**: This feature requires:\n- Places.Read.All permission\n- Meeting rooms configured in your organization\n\nError: ${findRoomsError.message}`,
481
+ text: `Unable to find meeting rooms.\n\n**Note**: ${explanation}\n\nError: ${errMsg}`,
466
482
  },
467
483
  ],
468
484
  };
@@ -603,6 +619,11 @@ const advancedTools = [
603
619
  type: 'string',
604
620
  description: 'Email address of the shared mailbox (required)',
605
621
  },
622
+ email: {
623
+ type: 'string',
624
+ description:
625
+ 'Alias for `sharedMailbox` (more intuitive name for the same value).',
626
+ },
606
627
  folder: {
607
628
  type: 'string',
608
629
  description: 'Folder to read from (default: inbox)',
@@ -617,7 +638,8 @@ const advancedTools = [
617
638
  description: 'Output detail level (default: standard)',
618
639
  },
619
640
  },
620
- required: ['sharedMailbox'],
641
+ additionalProperties: false,
642
+ required: [],
621
643
  },
622
644
  handler: handleAccessSharedMailbox,
623
645
  },
@@ -654,6 +676,7 @@ const advancedTools = [
654
676
  description: 'Output detail level (default: standard)',
655
677
  },
656
678
  },
679
+ additionalProperties: false,
657
680
  required: [],
658
681
  },
659
682
  handler: handleFindMeetingRooms,
@@ -49,11 +49,13 @@ class TokenStorage {
49
49
  }
50
50
  }
51
51
 
52
- if (!this.config.clientId || !this.config.clientSecret) {
52
+ if (!this.config.clientId) {
53
53
  console.warn(
54
- 'TokenStorage: OUTLOOK_CLIENT_ID or OUTLOOK_CLIENT_SECRET is not configured. Token operations might fail.'
54
+ 'TokenStorage: OUTLOOK_CLIENT_ID is not configured. Token operations will fail.'
55
55
  );
56
56
  }
57
+ // client_secret is only required for browser flow (confidential client).
58
+ // Device code flow (public client) does not use client_secret.
57
59
  }
58
60
 
59
61
  async _loadTokensFromFile() {
@@ -158,14 +160,24 @@ class TokenStorage {
158
160
  return this._refreshPromise.then((tokens) => tokens.access_token);
159
161
  }
160
162
 
161
- console.log('Attempting to refresh access token...');
162
- const postData = querystring.stringify({
163
+ // Device code flow is a public client flow — Microsoft rejects client_secret
164
+ // in refresh requests for tokens obtained via device code.
165
+ // Browser flow (confidential client) requires client_secret.
166
+ const isDeviceCode = this.tokens.auth_method === 'device-code';
167
+ console.log(
168
+ `Attempting to refresh access token (auth_method: ${this.tokens.auth_method || 'browser'})...`
169
+ );
170
+
171
+ const refreshParams = {
163
172
  client_id: this.config.clientId,
164
- client_secret: this.config.clientSecret,
165
173
  grant_type: 'refresh_token',
166
174
  refresh_token: this.tokens.refresh_token,
167
175
  scope: this.config.scopes.join(' '),
168
- });
176
+ };
177
+ if (!isDeviceCode) {
178
+ refreshParams.client_secret = this.config.clientSecret;
179
+ }
180
+ const postData = querystring.stringify(refreshParams);
169
181
 
170
182
  const requestOptions = {
171
183
  method: 'POST',
package/auth/tools.js CHANGED
@@ -2,9 +2,17 @@
2
2
  * Authentication-related tools for the Outlook Assistant server
3
3
  */
4
4
  const config = require('../config');
5
+ const fs = require('fs');
6
+ const path = require('path');
5
7
  const tokenManager = require('./token-manager');
6
8
  const { initiateDeviceCodeFlow, pollForToken } = require('./device-code');
7
9
 
10
+ // Path for persisting device code state across MCP server restarts
11
+ const DEVICE_CODE_STATE_PATH = path.join(
12
+ process.env.HOME || process.env.USERPROFILE,
13
+ '.outlook-assistant-pending-auth.json'
14
+ );
15
+
8
16
  // Dynamic tool count — set by index.js after TOOLS array is built
9
17
  let _toolCount = 0;
10
18
  function setToolCount(count) {
@@ -20,17 +28,40 @@ async function handleAbout() {
20
28
  (s) => s !== 'offline_access'
21
29
  );
22
30
  const testMode = config.USE_TEST_MODE ? 'Enabled' : 'Disabled';
31
+ const rateLimitConfigured = Boolean(
32
+ process.env.OUTLOOK_MAX_EMAILS_PER_SESSION
33
+ );
34
+ const allowlistConfigured = Boolean(process.env.OUTLOOK_ALLOWED_RECIPIENTS);
23
35
  const rateLimit =
24
36
  process.env.OUTLOOK_MAX_EMAILS_PER_SESSION || 'Unlimited (no limit set)';
25
37
  const allowlist =
26
38
  process.env.OUTLOOK_ALLOWED_RECIPIENTS || 'None (all recipients allowed)';
27
39
 
40
+ // F-2: surface the authenticated user's email so callers and AI
41
+ // agents can confirm which mailbox is connected. Uses a single
42
+ // GET /me round-trip when a valid token is available; degrades
43
+ // gracefully when not authenticated.
44
+ let identity = 'Not authenticated (run `auth action=authenticate`)';
45
+ try {
46
+ const { ensureAuthenticated } = require('./index');
47
+ const { callGraphAPI } = require('../utils/graph-api');
48
+ const token = await ensureAuthenticated();
49
+ const me = await callGraphAPI(token, 'GET', 'me', null, {
50
+ $select: 'userPrincipalName,mail,displayName',
51
+ });
52
+ const upn = me.mail || me.userPrincipalName;
53
+ identity = me.displayName ? `${me.displayName} <${upn}>` : upn;
54
+ } catch (_e) {
55
+ // Leave default identity message in place
56
+ }
57
+
28
58
  const lines = [
29
59
  `# Outlook Assistant Server v${config.SERVER_VERSION}\n`,
30
60
  `Provides access to Microsoft Outlook email, calendar, and contacts through Microsoft Graph API.\n`,
31
61
  `## Diagnostics\n`,
32
62
  `| Setting | Value |`,
33
63
  `|---------|-------|`,
64
+ `| Mailbox | ${identity} |`,
34
65
  `| Tools | ${_toolCount} across 9 modules |`,
35
66
  `| Modules | auth, email, calendar, folder, rules, contacts, categories, settings, advanced |`,
36
67
  `| Timezone | ${config.DEFAULT_TIMEZONE} |`,
@@ -42,6 +73,27 @@ async function handleAbout() {
42
73
  `**Scopes**: ${scopes.join(', ')}`,
43
74
  ];
44
75
 
76
+ // F-1 / F-48: warn when no safety belts are wired up. AI-assisted
77
+ // sending is significantly safer with a session rate limit and a
78
+ // recipient allowlist; both are off by default.
79
+ if (!rateLimitConfigured || !allowlistConfigured) {
80
+ lines.push('');
81
+ lines.push('## ⚠ Safety Belts Not Configured\n');
82
+ lines.push(
83
+ 'No rate limit or recipient allowlist is set. For safer AI-assisted sending, add to your `.mcp.json` env block:'
84
+ );
85
+ lines.push('```');
86
+ if (!rateLimitConfigured) {
87
+ lines.push('OUTLOOK_MAX_EMAILS_PER_SESSION=10');
88
+ }
89
+ if (!allowlistConfigured) {
90
+ lines.push(
91
+ 'OUTLOOK_ALLOWED_RECIPIENTS=your-domain.com,trusted@example.com'
92
+ );
93
+ }
94
+ lines.push('```');
95
+ }
96
+
45
97
  return {
46
98
  content: [
47
99
  {
@@ -89,12 +141,57 @@ async function handleAuthenticate(args) {
89
141
  };
90
142
  }
91
143
 
92
- // Module-level state for pending device code flow
144
+ // In-memory state for pending device code flow (also persisted to disk)
93
145
  let pendingDeviceCode = null;
94
146
 
147
+ /**
148
+ * Save device code state to disk so it survives MCP server restarts.
149
+ * Uses mode 0o600 (owner-only) — same as token file.
150
+ * @param {object|null} state - Device code state or null to delete
151
+ */
152
+ function saveDeviceCodeState(state) {
153
+ try {
154
+ if (state) {
155
+ fs.writeFileSync(DEVICE_CODE_STATE_PATH, JSON.stringify(state), {
156
+ mode: 0o600,
157
+ });
158
+ } else if (fs.existsSync(DEVICE_CODE_STATE_PATH)) {
159
+ fs.unlinkSync(DEVICE_CODE_STATE_PATH);
160
+ }
161
+ } catch (error) {
162
+ console.error(
163
+ `[AUTH] Failed to ${state ? 'save' : 'clean up'} device code state: ${error.message}`
164
+ );
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Load device code state from disk (fallback when in-memory state is lost).
170
+ * Returns null if no state exists or if the state has expired.
171
+ * @returns {object|null}
172
+ */
173
+ function loadDeviceCodeState() {
174
+ try {
175
+ if (!fs.existsSync(DEVICE_CODE_STATE_PATH)) {
176
+ return null;
177
+ }
178
+ const state = JSON.parse(fs.readFileSync(DEVICE_CODE_STATE_PATH, 'utf8'));
179
+ if (Date.now() > state.expiresAt) {
180
+ console.error('[AUTH] Persisted device code has expired, cleaning up');
181
+ saveDeviceCodeState(null);
182
+ return null;
183
+ }
184
+ return state;
185
+ } catch (error) {
186
+ console.error(`[AUTH] Failed to load device code state: ${error.message}`);
187
+ return null;
188
+ }
189
+ }
190
+
95
191
  /**
96
192
  * Device code flow step 1 — request a code for the user to enter.
97
193
  * Returns the code + URL immediately. Call device-code-complete to finish.
194
+ * State is persisted to disk so it survives MCP server restarts.
98
195
  * @returns {object} - MCP response
99
196
  */
100
197
  async function handleDeviceCodeAuth() {
@@ -116,13 +213,14 @@ async function handleDeviceCodeAuth() {
116
213
  config.AUTH_CONFIG.scopes
117
214
  );
118
215
 
119
- // Store for the completion step
216
+ // Store in memory and persist to disk
120
217
  pendingDeviceCode = {
121
218
  deviceCode: response.deviceCode,
122
219
  interval: response.interval,
123
220
  expiresIn: response.expiresIn,
124
221
  expiresAt: Date.now() + response.expiresIn * 1000,
125
222
  };
223
+ saveDeviceCodeState(pendingDeviceCode);
126
224
 
127
225
  console.error(
128
226
  `[AUTH] Device code: ${response.userCode}, expires in ${response.expiresIn}s`
@@ -146,9 +244,15 @@ async function handleDeviceCodeAuth() {
146
244
 
147
245
  /**
148
246
  * Device code flow step 2 — poll until the user completes authentication.
247
+ * Checks in-memory state first, falls back to disk-persisted state.
149
248
  * @returns {object} - MCP response
150
249
  */
151
250
  async function handleDeviceCodeComplete() {
251
+ // Try in-memory first, fall back to disk (survives server restarts)
252
+ if (!pendingDeviceCode) {
253
+ pendingDeviceCode = loadDeviceCodeState();
254
+ }
255
+
152
256
  if (!pendingDeviceCode) {
153
257
  return {
154
258
  content: [
@@ -162,6 +266,7 @@ async function handleDeviceCodeComplete() {
162
266
 
163
267
  if (Date.now() > pendingDeviceCode.expiresAt) {
164
268
  pendingDeviceCode = null;
269
+ saveDeviceCodeState(null);
165
270
  return {
166
271
  content: [
167
272
  {
@@ -184,8 +289,9 @@ async function handleDeviceCodeComplete() {
184
289
  );
185
290
 
186
291
  pendingDeviceCode = null;
292
+ saveDeviceCodeState(null);
187
293
 
188
- // Save tokens using TokenStorage
294
+ // Save tokens using TokenStorage — mark as device-code auth
189
295
  const TokenStorage = require('./token-storage');
190
296
  const tokenStorage = new TokenStorage({
191
297
  clientId: config.AUTH_CONFIG.clientId,
@@ -202,6 +308,7 @@ async function handleDeviceCodeComplete() {
202
308
  expires_at: Date.now() + tokenResponse.expires_in * 1000,
203
309
  scope: tokenResponse.scope,
204
310
  token_type: tokenResponse.token_type,
311
+ auth_method: 'device-code',
205
312
  };
206
313
  await tokenStorage._saveTokensToFile();
207
314
 
@@ -217,6 +324,7 @@ async function handleDeviceCodeComplete() {
217
324
  };
218
325
  } catch (error) {
219
326
  pendingDeviceCode = null;
327
+ saveDeviceCodeState(null);
220
328
  return {
221
329
  content: [
222
330
  {
@@ -305,6 +413,7 @@ const authTools = [
305
413
  'Force re-authentication even if already authenticated (action=authenticate only)',
306
414
  },
307
415
  },
416
+ additionalProperties: false,
308
417
  required: [],
309
418
  },
310
419
  handler: async (args) => {
@@ -317,8 +426,16 @@ const authTools = [
317
426
  case 'about':
318
427
  return handleAbout();
319
428
  case 'status':
320
- default:
321
429
  return handleCheckAuthStatus();
430
+ default:
431
+ return {
432
+ content: [
433
+ {
434
+ type: 'text',
435
+ text: `Unknown action '${action}'. Valid actions: status, authenticate, device-code-complete, about.`,
436
+ },
437
+ ],
438
+ };
322
439
  }
323
440
  },
324
441
  },
package/calendar/index.js CHANGED
@@ -25,6 +25,7 @@ const calendarTools = [
25
25
  description: 'Number of events to retrieve (default: 10, max: 50)',
26
26
  },
27
27
  },
28
+ additionalProperties: false,
28
29
  required: [],
29
30
  },
30
31
  handler: handleListEvents,
@@ -65,6 +66,7 @@ const calendarTools = [
65
66
  description: 'Optional body content for the event',
66
67
  },
67
68
  },
69
+ additionalProperties: false,
68
70
  required: ['subject', 'start', 'end'],
69
71
  },
70
72
  handler: handleCreateEvent,
@@ -91,14 +93,38 @@ const calendarTools = [
91
93
  type: 'string',
92
94
  description: 'The ID of the event',
93
95
  },
96
+ id: {
97
+ type: 'string',
98
+ description:
99
+ 'Alias for `eventId` (canonical per the v3.7.3 alias pass).',
100
+ },
94
101
  comment: {
95
102
  type: 'string',
96
103
  description: 'Optional comment for declining or cancelling the event',
97
104
  },
98
105
  },
99
- required: ['action', 'eventId'],
106
+ additionalProperties: false,
107
+ required: ['action'],
100
108
  },
101
109
  handler: async (args) => {
110
+ // F-37: accept `id` as alias for `eventId` so callers don't have
111
+ // to remember which tool uses which name. Both work; eventId
112
+ // remains the canonical Graph param.
113
+ const normalised = { ...args };
114
+ if (!normalised.eventId && normalised.id) {
115
+ normalised.eventId = normalised.id;
116
+ }
117
+ if (!normalised.eventId) {
118
+ return {
119
+ content: [
120
+ {
121
+ type: 'text',
122
+ text: 'Required parameter `eventId` (or alias `id`) is missing.',
123
+ },
124
+ ],
125
+ };
126
+ }
127
+ args = normalised;
102
128
  switch (args.action) {
103
129
  case 'decline':
104
130
  return handleDeclineEvent(args);