@littlebearapps/outlook-assistant 3.12.1 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
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>
18
18
  </p>
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.
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, GitHub Copilot, Cursor, Windsurf, and any MCP-compatible client.
21
21
 
22
22
  **Works with personal Outlook.com and work/school Microsoft 365 accounts.**
23
23
 
@@ -135,7 +135,6 @@ Outlook Assistant is designed with safety-first principles for AI-driven email a
135
135
  > ```json
136
136
  > "env": {
137
137
  > "OUTLOOK_CLIENT_ID": "…",
138
- > "OUTLOOK_CLIENT_SECRET": "…",
139
138
  > "OUTLOOK_MAX_EMAILS_PER_SESSION": "10",
140
139
  > "OUTLOOK_ALLOWED_RECIPIENTS": "your-domain.com,trusted@example.com"
141
140
  > }
@@ -166,7 +165,7 @@ npx @littlebearapps/outlook-assistant
166
165
  To check which version you have, or to see the available options:
167
166
 
168
167
  ```bash
169
- outlook-assistant --version # prints e.g. 3.12.1
168
+ outlook-assistant --version # prints e.g. 3.13.0
170
169
  outlook-assistant --help # usage, options and key environment variables
171
170
  ```
172
171
 
@@ -180,14 +179,23 @@ You need a Microsoft Azure app registration to authenticate. See the **[Azure Se
180
179
 
181
180
  1. Create a new app registration at [portal.azure.com](https://portal.azure.com/)
182
181
  2. Add Microsoft Graph delegated permissions (Mail, Calendar, Contacts)
183
- 3. Create a client secret and copy the **Value** (not the Secret ID)
182
+ 3. _(Browser flow only)_ Create a client secret and copy the **Value** (not the Secret ID). The default device-code sign-in doesn't need one
184
183
  4. Under Authentication > **Add a platform** > **Mobile and desktop applications** — check `nativeclient` URI
185
184
  5. Enable **"Allow public client flows"** in Authentication > Advanced settings
186
185
  6. _(Optional)_ Set redirect URI to `http://localhost:3333/auth/callback` — only needed for browser auth flow
187
186
 
188
187
  ### 3. Configure Your MCP Client
189
188
 
190
- Add to your MCP client config:
189
+ **Plugin install (Claude Code).** The plugin bundles the server pinned to an exact version and asks for your settings when you enable it:
190
+
191
+ ```bash
192
+ claude plugin marketplace add littlebearapps/outlook-assistant
193
+ claude plugin install outlook-assistant@littlebearapps
194
+ ```
195
+
196
+ The same plugin folder ([`plugins/outlook-assistant`](plugins/outlook-assistant/)) also follows the [Agent Plugins](https://agent-plugins.org/) format used by GitHub Copilot and Cursor.
197
+
198
+ **Manual config.** Add to your MCP client config. Only `OUTLOOK_CLIENT_ID` is needed for the default device-code sign-in; add `OUTLOOK_CLIENT_SECRET` only if you use the [browser flow](#browser-redirect-flow-alternative). You can also leave the client ID out and give it to your assistant when you first connect (`auth action=authenticate clientId=…`), which saves it to `~/.outlook-assistant-config.json`. An `OUTLOOK_CLIENT_ID` in the environment always takes precedence.
191
199
 
192
200
  <details>
193
201
  <summary><strong>Claude Desktop</strong> (<code>claude_desktop_config.json</code>)</summary>
@@ -199,8 +207,7 @@ Add to your MCP client config:
199
207
  "command": "npx",
200
208
  "args": ["@littlebearapps/outlook-assistant"],
201
209
  "env": {
202
- "OUTLOOK_CLIENT_ID": "your-application-client-id",
203
- "OUTLOOK_CLIENT_SECRET": "your-client-secret-VALUE"
210
+ "OUTLOOK_CLIENT_ID": "your-application-client-id"
204
211
  }
205
212
  }
206
213
  }
@@ -214,17 +221,46 @@ Add to your MCP client config:
214
221
  ```bash
215
222
  claude mcp add outlook \
216
223
  -e OUTLOOK_CLIENT_ID=your-application-client-id \
217
- -e OUTLOOK_CLIENT_SECRET=your-client-secret-VALUE \
218
224
  -- npx -y @littlebearapps/outlook-assistant
219
225
  ```
220
226
 
221
227
  The MCP server reads its settings from the environment your client passes it; it doesn't load a `.env` file.
222
228
  </details>
223
229
 
230
+ <details>
231
+ <summary><strong>VS Code / GitHub Copilot</strong> (<code>.vscode/mcp.json</code>)</summary>
232
+
233
+ VS Code prompts for the client ID the first time the server starts and stores it securely:
234
+
235
+ ```json
236
+ {
237
+ "inputs": [
238
+ {
239
+ "type": "promptString",
240
+ "id": "outlook-client-id",
241
+ "description": "Azure application (client) ID"
242
+ }
243
+ ],
244
+ "servers": {
245
+ "outlook": {
246
+ "type": "stdio",
247
+ "command": "npx",
248
+ "args": ["-y", "@littlebearapps/outlook-assistant"],
249
+ "env": {
250
+ "OUTLOOK_CLIENT_ID": "${input:outlook-client-id}"
251
+ }
252
+ }
253
+ }
254
+ }
255
+ ```
256
+
257
+ Use it from Copilot Chat in **Agent** mode. To use it in every workspace, add the same entry to your user `mcp.json` (Command Palette → **MCP: Open User Configuration**).
258
+ </details>
259
+
224
260
  <details>
225
261
  <summary><strong>Cursor</strong> (<code>.cursor/mcp.json</code>)</summary>
226
262
 
227
- [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=Outlook%20Assistant&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsaXR0bGViZWFyYXBwcy9vdXRsb29rLWFzc2lzdGFudCJdLCJlbnYiOnsiT1VUTE9PS19DTElFTlRfSUQiOiIiLCJPVVRMT09LX0NMSUVOVF9TRUNSRVQiOiIifX0=)
263
+ [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=Outlook%20Assistant&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsaXR0bGViZWFyYXBwcy9vdXRsb29rLWFzc2lzdGFudCJdLCJlbnYiOnsiT1VUTE9PS19DTElFTlRfSUQiOiIifX0=)
228
264
 
229
265
  Or add manually to `.cursor/mcp.json`:
230
266
 
@@ -235,8 +271,7 @@ Or add manually to `.cursor/mcp.json`:
235
271
  "command": "npx",
236
272
  "args": ["@littlebearapps/outlook-assistant"],
237
273
  "env": {
238
- "OUTLOOK_CLIENT_ID": "your-application-client-id",
239
- "OUTLOOK_CLIENT_SECRET": "your-client-secret-VALUE"
274
+ "OUTLOOK_CLIENT_ID": "your-application-client-id"
240
275
  }
241
276
  }
242
277
  }
@@ -254,8 +289,7 @@ Or add manually to `.cursor/mcp.json`:
254
289
  "command": "npx",
255
290
  "args": ["@littlebearapps/outlook-assistant"],
256
291
  "env": {
257
- "OUTLOOK_CLIENT_ID": "your-application-client-id",
258
- "OUTLOOK_CLIENT_SECRET": "your-client-secret-VALUE"
292
+ "OUTLOOK_CLIENT_ID": "your-application-client-id"
259
293
  }
260
294
  }
261
295
  }
@@ -339,6 +373,8 @@ a server that would ignore it.
339
373
 
340
374
  ### Create a Client Secret
341
375
 
376
+ Only needed for the [browser redirect flow](#browser-redirect-flow-alternative). Skip this if you sign in with the default device code.
377
+
342
378
  1. Go to **Certificates & secrets** > **New client secret**
343
379
  2. Enter a description and select expiration
344
380
  3. Click **Add**
@@ -378,7 +414,7 @@ USE_TEST_MODE=false
378
414
 
379
415
  ### MCP Client Configuration
380
416
 
381
- See [Quick Start — Configure Your MCP Client](#3-configure-your-mcp-client) above for Claude Desktop, Claude Code, Cursor, and Windsurf configs.
417
+ See [Quick Start — Configure Your MCP Client](#3-configure-your-mcp-client) above for Claude Desktop, Claude Code, VS Code / GitHub Copilot, Cursor, and Windsurf configs.
382
418
 
383
419
  If installed from source, use `node` instead of `npx`:
384
420
 
@@ -389,8 +425,7 @@ If installed from source, use `node` instead of `npx`:
389
425
  "command": "node",
390
426
  "args": ["/path/to/outlook-assistant/index.js"],
391
427
  "env": {
392
- "OUTLOOK_CLIENT_ID": "your-application-client-id",
393
- "OUTLOOK_CLIENT_SECRET": "your-client-secret-VALUE"
428
+ "OUTLOOK_CLIENT_ID": "your-application-client-id"
394
429
  }
395
430
  }
396
431
  }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Runtime-supplied Azure Application (client) ID.
3
+ *
4
+ * Plugin marketplaces (GitHub Copilot, Cursor — Agent Plugins 1.0) ship a
5
+ * static `mcp.json` with no way to prompt for settings, so users there can't
6
+ * set OUTLOOK_CLIENT_ID. Device-code sign-in only needs the client ID (never
7
+ * the secret), so the `auth` tool accepts it at runtime and it's persisted to
8
+ * `~/.outlook-assistant-config.json`.
9
+ *
10
+ * Precedence: OUTLOOK_CLIENT_ID env → MS_CLIENT_ID env (legacy) → saved file.
11
+ *
12
+ * Deliberately does NOT require ../config (config.js requires this module).
13
+ */
14
+ const fs = require('fs');
15
+ const os = require('os');
16
+ const path = require('path');
17
+
18
+ const CONFIG_FILE_NAME = '.outlook-assistant-config.json';
19
+
20
+ // Azure Application (client) IDs are GUIDs.
21
+ const CLIENT_ID_RE =
22
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
23
+
24
+ /**
25
+ * Path of the persisted config file. Resolved per call (not at load) so a
26
+ * changed HOME — e.g. in tests — is honoured.
27
+ * @returns {string}
28
+ */
29
+ function getConfigPath() {
30
+ const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir();
31
+ return path.join(homeDir, CONFIG_FILE_NAME);
32
+ }
33
+
34
+ /**
35
+ * @param {unknown} id
36
+ * @returns {boolean} - true when `id` (trimmed) is a GUID
37
+ */
38
+ function isValidClientId(id) {
39
+ return typeof id === 'string' && CLIENT_ID_RE.test(id.trim());
40
+ }
41
+
42
+ /**
43
+ * Read the whole config file. Never throws.
44
+ * @returns {object} - Parsed object, or {} when missing/unreadable/not an object
45
+ */
46
+ function readConfigFile() {
47
+ try {
48
+ const parsed = JSON.parse(fs.readFileSync(getConfigPath(), 'utf8'));
49
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
50
+ ? parsed
51
+ : {};
52
+ } catch {
53
+ return {};
54
+ }
55
+ }
56
+
57
+ /**
58
+ * The saved client ID, or '' when there is none or it isn't a valid GUID.
59
+ * Never throws.
60
+ * @returns {string}
61
+ */
62
+ function loadSavedClientId() {
63
+ const { clientId } = readConfigFile();
64
+ return isValidClientId(clientId) ? clientId.trim() : '';
65
+ }
66
+
67
+ /**
68
+ * Validate and persist a client ID (mode 0600, temp file + rename), keeping
69
+ * any other keys already in the file.
70
+ * @param {string} id
71
+ * @returns {string} - The normalised (trimmed) ID that was saved
72
+ * @throws {Error} - When `id` isn't a GUID or the write fails
73
+ */
74
+ function saveClientId(id) {
75
+ if (!isValidClientId(id)) {
76
+ throw new Error(
77
+ 'Invalid client ID: expected the Application (client) ID GUID from your Azure app registration.'
78
+ );
79
+ }
80
+ const clientId = id.trim();
81
+ const filePath = getConfigPath();
82
+ const data = { ...readConfigFile(), clientId };
83
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
84
+ try {
85
+ fs.writeFileSync(tmpPath, `${JSON.stringify(data, null, 2)}\n`, {
86
+ mode: 0o600,
87
+ flag: 'wx',
88
+ });
89
+ fs.renameSync(tmpPath, filePath);
90
+ } catch (error) {
91
+ try {
92
+ fs.unlinkSync(tmpPath);
93
+ } catch {
94
+ // Temp file was never created or is already gone
95
+ }
96
+ throw error;
97
+ }
98
+ return clientId;
99
+ }
100
+
101
+ /**
102
+ * Which environment variable supplies the client ID, if any.
103
+ * @returns {{name: string, value: string}|null}
104
+ */
105
+ function getEnvClientId() {
106
+ // Trimmed, and blank counts as unset: plugin managers pass "" (or stray
107
+ // whitespace) for an unset option, which must not hide a saved ID.
108
+ for (const name of ['OUTLOOK_CLIENT_ID', 'MS_CLIENT_ID']) {
109
+ const value = (process.env[name] || '').trim();
110
+ if (value) return { name, value };
111
+ }
112
+ return null;
113
+ }
114
+
115
+ /**
116
+ * Effective client ID: env (OUTLOOK_CLIENT_ID, then legacy MS_CLIENT_ID),
117
+ * then the saved file, then ''.
118
+ * @returns {string}
119
+ */
120
+ function resolveClientId() {
121
+ const env = getEnvClientId();
122
+ return env ? env.value : loadSavedClientId();
123
+ }
124
+
125
+ /**
126
+ * @returns {'env'|'saved'|'none'} - Where the effective client ID comes from
127
+ */
128
+ function getClientIdSource() {
129
+ if (getEnvClientId()) return 'env';
130
+ return loadSavedClientId() ? 'saved' : 'none';
131
+ }
132
+
133
+ module.exports = {
134
+ CONFIG_FILE_NAME,
135
+ getConfigPath,
136
+ isValidClientId,
137
+ loadSavedClientId,
138
+ saveClientId,
139
+ getEnvClientId,
140
+ resolveClientId,
141
+ getClientIdSource,
142
+ };
package/auth/index.js CHANGED
@@ -6,9 +6,11 @@ const TokenStorage = require('./token-storage');
6
6
  const config = require('../config');
7
7
  const { authTools, setToolCount } = require('./tools');
8
8
 
9
- // Singleton TokenStorage instance with auto-refresh support
9
+ // Singleton TokenStorage instance with auto-refresh support. No clientId is
10
+ // passed: TokenStorage resolves it on each use (env → saved config file), so
11
+ // an ID saved at runtime via `auth action=authenticate clientId=…` applies
12
+ // without a restart.
10
13
  const tokenStorage = new TokenStorage({
11
- clientId: config.AUTH_CONFIG.clientId,
12
14
  clientSecret: config.AUTH_CONFIG.clientSecret,
13
15
  tokenStorePath: config.AUTH_CONFIG.tokenStorePath,
14
16
  scopes: config.AUTH_CONFIG.scopes,
@@ -4,6 +4,7 @@ const _https = require('https'); // Reserved for future HTTPS support
4
4
  const _fs = require('fs'); // Reserved for future HTTPS support
5
5
  const crypto = require('crypto'); // Added for generating random string
6
6
  const TokenStorage = require('./token-storage'); // Assuming TokenStorage is in the same directory
7
+ const { loadSavedClientId } = require('./client-config');
7
8
 
8
9
  // HTML templates
9
10
  function escapeHtml(unsafe) {
@@ -54,8 +55,11 @@ const templates = {
54
55
 
55
56
  function createAuthConfig(envPrefix = 'OUTLOOK_') {
56
57
  return {
58
+ // Env first; then the ID saved via `auth action=authenticate clientId=…`.
57
59
  clientId:
58
- process.env[`${envPrefix}CLIENT_ID`] || process.env.MS_CLIENT_ID || '',
60
+ process.env[`${envPrefix}CLIENT_ID`] ||
61
+ process.env.MS_CLIENT_ID ||
62
+ loadSavedClientId(),
59
63
  clientSecret:
60
64
  process.env[`${envPrefix}CLIENT_SECRET`] ||
61
65
  process.env.MS_CLIENT_SECRET ||
@@ -4,6 +4,7 @@ const path = require('path');
4
4
  const https = require('https');
5
5
  const querystring = require('querystring');
6
6
  const { describeAuthError } = require('./auth-errors');
7
+ const { resolveClientId } = require('./client-config');
7
8
 
8
9
  /**
9
10
  * Decide which scopes a refresh request should use. Prefer the scopes that were
@@ -42,7 +43,9 @@ class TokenStorage {
42
43
  process.env.HOME || process.env.USERPROFILE,
43
44
  '.outlook-assistant-tokens.json'
44
45
  ),
45
- clientId: process.env.OUTLOOK_CLIENT_ID || process.env.MS_CLIENT_ID,
46
+ // No default clientId: when none is passed, getClientId() resolves it on
47
+ // every use (env → saved ~/.outlook-assistant-config.json), so an ID
48
+ // saved at runtime via the auth tool takes effect without a restart.
46
49
  clientSecret:
47
50
  process.env.OUTLOOK_CLIENT_SECRET || process.env.MS_CLIENT_SECRET,
48
51
  redirectUri:
@@ -80,7 +83,7 @@ class TokenStorage {
80
83
  }
81
84
  }
82
85
 
83
- if (!this.config.clientId) {
86
+ if (!this.getClientId()) {
84
87
  console.warn(
85
88
  'TokenStorage: OUTLOOK_CLIENT_ID is not configured. Token operations will fail.'
86
89
  );
@@ -89,6 +92,15 @@ class TokenStorage {
89
92
  // Device code flow (public client) does not use client_secret.
90
93
  }
91
94
 
95
+ /**
96
+ * Client ID for token requests: an explicitly configured value wins,
97
+ * otherwise it is resolved now (OUTLOOK_CLIENT_ID → MS_CLIENT_ID → saved).
98
+ * @returns {string}
99
+ */
100
+ getClientId() {
101
+ return this.config.clientId || resolveClientId();
102
+ }
103
+
92
104
  async _loadTokensFromFile() {
93
105
  try {
94
106
  const tokenData = await fs.readFile(this.config.tokenStorePath, 'utf8');
@@ -149,12 +161,12 @@ class TokenStorage {
149
161
  await this.getTokens(); // Ensure tokens are loaded
150
162
 
151
163
  if (!this.tokens || !this.tokens.access_token) {
152
- console.log('No access token available.');
164
+ console.error('No access token available.');
153
165
  return null;
154
166
  }
155
167
 
156
168
  if (this.isTokenExpired()) {
157
- console.log(
169
+ console.error(
158
170
  'Access token expired or nearing expiration. Attempting refresh.'
159
171
  );
160
172
  if (this.tokens.refresh_token) {
@@ -193,7 +205,7 @@ class TokenStorage {
193
205
 
194
206
  // Prevent multiple concurrent refresh attempts
195
207
  if (this._refreshPromise) {
196
- console.log('Refresh already in progress, returning existing promise.');
208
+ console.error('Refresh already in progress, returning existing promise.');
197
209
  return this._refreshPromise.then((tokens) => tokens.access_token);
198
210
  }
199
211
 
@@ -201,12 +213,12 @@ class TokenStorage {
201
213
  // in refresh requests for tokens obtained via device code.
202
214
  // Browser flow (confidential client) requires client_secret.
203
215
  const isDeviceCode = this.tokens.auth_method === 'device-code';
204
- console.log(
216
+ console.error(
205
217
  `Attempting to refresh access token (auth_method: ${this.tokens.auth_method || 'browser'})...`
206
218
  );
207
219
 
208
220
  const refreshParams = {
209
- client_id: this.config.clientId,
221
+ client_id: this.getClientId(),
210
222
  grant_type: 'refresh_token',
211
223
  refresh_token: this.tokens.refresh_token,
212
224
  // Use the GRANTED scopes, not the full configured set. After a base-only
@@ -247,7 +259,9 @@ class TokenStorage {
247
259
  Date.now() + responseBody.expires_in * 1000;
248
260
  try {
249
261
  await this._saveTokensToFile();
250
- console.log('Access token refreshed and saved successfully.');
262
+ console.error(
263
+ 'Access token refreshed and saved successfully.'
264
+ );
251
265
  resolve(this.tokens);
252
266
  } catch (saveError) {
253
267
  console.error('Failed to save refreshed tokens:', saveError);
@@ -298,15 +312,16 @@ class TokenStorage {
298
312
  }
299
313
 
300
314
  async exchangeCodeForTokens(authCode) {
301
- if (!this.config.clientId || !this.config.clientSecret) {
315
+ const clientId = this.getClientId();
316
+ if (!clientId || !this.config.clientSecret) {
302
317
  throw new Error(
303
318
  'Client ID or Client Secret is not configured. Cannot exchange code for tokens.'
304
319
  );
305
320
  }
306
- console.log('Exchanging authorization code for tokens...');
321
+ console.error('Exchanging authorization code for tokens...');
307
322
  const requestedScopes = this.config.scopes;
308
323
  const postData = querystring.stringify({
309
- client_id: this.config.clientId,
324
+ client_id: clientId,
310
325
  client_secret: this.config.clientSecret,
311
326
  grant_type: 'authorization_code',
312
327
  code: authCode,
@@ -351,7 +366,7 @@ class TokenStorage {
351
366
  };
352
367
  try {
353
368
  await this._saveTokensToFile();
354
- console.log('Tokens exchanged and saved successfully.');
369
+ console.error('Tokens exchanged and saved successfully.');
355
370
  resolve(this.tokens);
356
371
  } catch (saveError) {
357
372
  console.error('Failed to save exchanged tokens:', saveError);
@@ -408,10 +423,10 @@ class TokenStorage {
408
423
  this.tokens = null;
409
424
  try {
410
425
  await fs.unlink(this.config.tokenStorePath);
411
- console.log('Token file deleted successfully.');
426
+ console.error('Token file deleted successfully.');
412
427
  } catch (error) {
413
428
  if (error.code === 'ENOENT') {
414
- console.log('Token file not found, nothing to delete.');
429
+ console.error('Token file not found, nothing to delete.');
415
430
  } else {
416
431
  console.error('Error deleting token file:', error);
417
432
  }
package/auth/tools.js CHANGED
@@ -6,6 +6,13 @@ const { getAuthErrorHints } = require('./auth-errors');
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const tokenManager = require('./token-manager');
9
+ const {
10
+ CONFIG_FILE_NAME,
11
+ isValidClientId,
12
+ saveClientId,
13
+ getEnvClientId,
14
+ getClientIdSource,
15
+ } = require('./client-config');
9
16
  const {
10
17
  initiateDeviceCodeFlow,
11
18
  pollForToken,
@@ -19,6 +26,112 @@ const DEVICE_CODE_STATE_PATH = path.join(
19
26
  '.outlook-assistant-pending-auth.json'
20
27
  );
21
28
 
29
+ const SETUP_GUIDE_URL =
30
+ 'https://github.com/littlebearapps/outlook-assistant/blob/main/docs/how-to/getting-started/connect-outlook-to-claude.md';
31
+ const SAVED_CONFIG_DISPLAY_PATH = `~/${CONFIG_FILE_NAME}`;
32
+
33
+ /**
34
+ * Error shown when no client ID resolves (no env var, nothing saved). Written
35
+ * for the AI client: it tells it what to ask the user and which call to make.
36
+ * @returns {object} - MCP response ({ content, isError: true })
37
+ */
38
+ function buildMissingClientIdResponse() {
39
+ return {
40
+ content: [
41
+ {
42
+ type: 'text',
43
+ text: [
44
+ 'Error: OUTLOOK_CLIENT_ID is not configured, so sign-in cannot start.',
45
+ '',
46
+ '1. Ask the user for the **Application (client) ID** of their Azure app registration (Azure portal → App registrations → their app → Overview). It is a GUID such as `00000000-0000-0000-0000-000000000000`.',
47
+ `2. Call \`auth action=authenticate clientId=<id>\`. The ID is saved to \`${SAVED_CONFIG_DISPLAY_PATH}\` (it is not a secret) and device-code sign-in starts.`,
48
+ '',
49
+ 'The client secret is not needed for device-code sign-in (the default); only the browser flow uses it.',
50
+ `No app registration yet? Follow the setup guide: ${SETUP_GUIDE_URL}`,
51
+ 'Alternatively, set OUTLOOK_CLIENT_ID in the MCP server environment and restart it.',
52
+ ].join('\n'),
53
+ },
54
+ ],
55
+ isError: true,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Validate and save a client ID supplied via `auth action=authenticate`.
61
+ * @param {unknown} clientId
62
+ * @returns {{error: object}|{saved: string}} - An MCP error response, or the saved ID
63
+ */
64
+ function applyClientIdArg(clientId) {
65
+ if (!isValidClientId(clientId)) {
66
+ return {
67
+ error: {
68
+ content: [
69
+ {
70
+ type: 'text',
71
+ text: [
72
+ 'Error: `clientId` is not a valid Azure Application (client) ID.',
73
+ '',
74
+ 'It must be the GUID shown as **Application (client) ID** on the app registration Overview page in the Azure portal, e.g. `00000000-0000-0000-0000-000000000000`. Do not use the Directory (tenant) ID, the Object ID or a client secret.',
75
+ `Setup guide: ${SETUP_GUIDE_URL}`,
76
+ ].join('\n'),
77
+ },
78
+ ],
79
+ isError: true,
80
+ },
81
+ };
82
+ }
83
+
84
+ const env = getEnvClientId();
85
+ if (env && env.value.trim().toLowerCase() !== clientId.trim().toLowerCase()) {
86
+ return {
87
+ error: {
88
+ content: [
89
+ {
90
+ type: 'text',
91
+ text: [
92
+ `Error: the ${env.name} environment variable is set to a different client ID, and it takes precedence over a saved one, so the \`clientId\` you supplied would be ignored.`,
93
+ '',
94
+ `To use the new ID, change or remove ${env.name} in the MCP server configuration, restart the server, then call \`auth action=authenticate\` again. Nothing was saved.`,
95
+ ].join('\n'),
96
+ },
97
+ ],
98
+ isError: true,
99
+ },
100
+ };
101
+ }
102
+
103
+ try {
104
+ return { saved: saveClientId(clientId) };
105
+ } catch (error) {
106
+ return {
107
+ error: {
108
+ content: [
109
+ {
110
+ type: 'text',
111
+ text: `Error: could not save the client ID to ${SAVED_CONFIG_DISPLAY_PATH}: ${error.message}`,
112
+ },
113
+ ],
114
+ isError: true,
115
+ },
116
+ };
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Client ID row for `auth about`. The ID itself is never shown.
122
+ * @returns {string}
123
+ */
124
+ function describeClientIdStatus() {
125
+ const source = getClientIdSource();
126
+ if (source === 'env') {
127
+ return `Configured (environment: ${getEnvClientId().name})`;
128
+ }
129
+ if (source === 'saved') {
130
+ return `Configured (saved in ${SAVED_CONFIG_DISPLAY_PATH})`;
131
+ }
132
+ return 'Not set (run `auth action=authenticate clientId=<Application (client) ID>`)';
133
+ }
134
+
22
135
  // Dynamic tool count — set by index.js after TOOLS array is built
23
136
  let _toolCount = 0;
24
137
  function setToolCount(count) {
@@ -122,6 +235,7 @@ async function handleAbout() {
122
235
  `| Setting | Value |`,
123
236
  `|---------|-------|`,
124
237
  `| Mailbox | ${identity} |`,
238
+ `| Client ID | ${describeClientIdStatus()} |`,
125
239
  `| Tools | ${_toolCount} across 9 modules |`,
126
240
  `| Modules | auth, email, calendar, folder, rules, contacts, categories, settings, advanced |`,
127
241
  `| Timezone | ${config.DEFAULT_TIMEZONE} |`,
@@ -185,19 +299,54 @@ async function handleAuthenticate(args) {
185
299
  };
186
300
  }
187
301
 
302
+ // Optional runtime client ID (for clients that can't set env vars, e.g.
303
+ // plugin marketplaces): validate, refuse if an env var would override it,
304
+ // save, then carry on with the normal flow.
305
+ // null / blank counts as not supplied: some clients send empty optionals.
306
+ let savedPrefix;
307
+ const suppliedClientId = args?.clientId;
308
+ if (
309
+ suppliedClientId !== undefined &&
310
+ suppliedClientId !== null &&
311
+ String(suppliedClientId).trim() !== ''
312
+ ) {
313
+ const result = applyClientIdArg(suppliedClientId);
314
+ if (result.error) {
315
+ return result.error;
316
+ }
317
+ savedPrefix = `Saved your Azure Application (client) ID to \`${SAVED_CONFIG_DISPLAY_PATH}\`.`;
318
+ }
319
+
188
320
  const method = args?.method || config.AUTH_CONFIG.defaultAuthMethod;
189
321
 
190
322
  if (method === 'device-code') {
191
- return handleDeviceCodeAuth();
323
+ return handleDeviceCodeAuth(savedPrefix);
192
324
  }
193
325
 
194
326
  // Browser redirect flow (existing behaviour)
195
- const authUrl = `${config.AUTH_CONFIG.authServerUrl}/auth?client_id=${config.AUTH_CONFIG.clientId}`;
327
+ const clientId = config.AUTH_CONFIG.clientId;
328
+ if (!clientId) {
329
+ return buildMissingClientIdResponse();
330
+ }
331
+ const authUrl = `${config.AUTH_CONFIG.authServerUrl}/auth?client_id=${encodeURIComponent(clientId)}`;
332
+ const lines = [];
333
+ if (savedPrefix) {
334
+ lines.push(savedPrefix, '');
335
+ }
336
+ lines.push(
337
+ `Authentication required. Please visit the following URL to authenticate with Microsoft: ${authUrl}\n\nAfter authentication, you will be redirected back to this application.\n\nNote: The auth server must be running on port 3333. If working remotely, consider using method=device-code instead.`
338
+ );
339
+ if (getClientIdSource() !== 'env') {
340
+ lines.push(
341
+ '',
342
+ 'The browser flow also needs the client secret: the auth server (`npm run auth-server`) reads OUTLOOK_CLIENT_ID and OUTLOOK_CLIENT_SECRET from its own environment and does not use a saved client ID. Device-code sign-in (the default) needs only the client ID.'
343
+ );
344
+ }
196
345
  return {
197
346
  content: [
198
347
  {
199
348
  type: 'text',
200
- text: `Authentication required. Please visit the following URL to authenticate with Microsoft: ${authUrl}\n\nAfter authentication, you will be redirected back to this application.\n\nNote: The auth server must be running on port 3333. If working remotely, consider using method=device-code instead.`,
349
+ text: lines.join('\n'),
201
350
  },
202
351
  ],
203
352
  };
@@ -254,26 +403,20 @@ function loadDeviceCodeState() {
254
403
  * Device code flow step 1 — request a code for the user to enter.
255
404
  * Returns the code + URL immediately. Call device-code-complete to finish.
256
405
  * State is persisted to disk so it survives MCP server restarts.
406
+ * @param {string} [prefix] - Optional leading line (e.g. "client ID saved")
257
407
  * @returns {object} - MCP response
258
408
  */
259
- async function handleDeviceCodeAuth() {
409
+ async function handleDeviceCodeAuth(prefix) {
260
410
  const clientId = config.AUTH_CONFIG.clientId;
261
411
  if (!clientId) {
262
- return {
263
- content: [
264
- {
265
- type: 'text',
266
- text: 'Error: OUTLOOK_CLIENT_ID is not configured.',
267
- },
268
- ],
269
- };
412
+ return buildMissingClientIdResponse();
270
413
  }
271
414
 
272
415
  console.error('[AUTH] Starting device code flow...');
273
416
  // Attempt the configured scope set (base, plus `.Shared` when
274
417
  // OUTLOOK_SHARED_MAILBOX opts in). If the account can't consent to
275
418
  // `.Shared`, handleDeviceCodeComplete re-issues with base scopes.
276
- return initiateDeviceCode(config.AUTH_CONFIG.scopes, 'full');
419
+ return initiateDeviceCode(config.AUTH_CONFIG.scopes, 'full', prefix);
277
420
  }
278
421
 
279
422
  /**
@@ -318,13 +461,15 @@ async function initiateDeviceCode(scopes, scopesUsed, prefix) {
318
461
  return buildDeviceCodeErrorResponse(error);
319
462
  }
320
463
 
321
- // Store in memory and persist to disk
464
+ // Store in memory and persist to disk. The client ID is recorded because
465
+ // the device code is bound to it: completion must poll with the same one.
322
466
  pendingDeviceCode = {
323
467
  deviceCode: response.deviceCode,
324
468
  interval: response.interval,
325
469
  expiresIn: response.expiresIn,
326
470
  expiresAt: Date.now() + response.expiresIn * 1000,
327
471
  scopesUsed,
472
+ clientId,
328
473
  };
329
474
  saveDeviceCodeState(pendingDeviceCode);
330
475
 
@@ -430,7 +575,14 @@ async function handleDeviceCodeComplete() {
430
575
  };
431
576
  }
432
577
 
433
- const clientId = config.AUTH_CONFIG.clientId;
578
+ // Poll with the client ID the code was issued to (older state files don't
579
+ // record it, so fall back to the current one).
580
+ const clientId = pendingDeviceCode.clientId || config.AUTH_CONFIG.clientId;
581
+ if (!clientId) {
582
+ pendingDeviceCode = null;
583
+ saveDeviceCodeState(null);
584
+ return buildMissingClientIdResponse();
585
+ }
434
586
  // Capture which scope set this pending flow attempted, before any mutation.
435
587
  const scopesUsed = pendingDeviceCode.scopesUsed || 'full';
436
588
  // The scopes we attempted — used as the granted_scopes fallback when the
@@ -455,7 +607,7 @@ async function handleDeviceCodeComplete() {
455
607
  // Save tokens using TokenStorage — mark as device-code auth
456
608
  const TokenStorage = require('./token-storage');
457
609
  const tokenStorage = new TokenStorage({
458
- clientId: config.AUTH_CONFIG.clientId,
610
+ clientId,
459
611
  clientSecret: config.AUTH_CONFIG.clientSecret,
460
612
  tokenStorePath: config.AUTH_CONFIG.tokenStorePath,
461
613
  scopes: config.AUTH_CONFIG.scopes,
@@ -593,8 +745,12 @@ async function handleCheckAuthStatus() {
593
745
 
594
746
  if (!accessToken) {
595
747
  console.error('[CHECK-AUTH-STATUS] No valid access token');
748
+ const text =
749
+ getClientIdSource() === 'none'
750
+ ? `Not authenticated. No Azure Application (client) ID is configured yet: ask the user for the Application (client) ID of their Azure app registration, then call \`auth action=authenticate clientId=<id>\`. Setup guide: ${SETUP_GUIDE_URL}`
751
+ : 'Not authenticated';
596
752
  return {
597
- content: [{ type: 'text', text: 'Not authenticated' }],
753
+ content: [{ type: 'text', text }],
598
754
  };
599
755
  }
600
756
 
@@ -622,7 +778,7 @@ const authTools = [
622
778
  {
623
779
  name: 'auth',
624
780
  description:
625
- 'Manage authentication with the Microsoft Graph API. action=`status` (default) returns the current auth state and auto-refreshes the access token if it\'s expired but the refresh token is still valid (~90-day window) — call this first to check before other tools. action=`authenticate` starts the OAuth flow: with `method: "device-code"` (default, works headlessly) it returns a code + URL for the user to visit; with `method: "browser"` it opens the local auth server on :3333 (run `npm run auth-server` first). Pass `force: true` to re-authenticate over an existing valid session. action=`device-code-complete` finishes device-code auth after the user enters the code in their browser — call this once authentication shows as successful in the browser. action=`about` returns server version, configured audience, scope list, and other diagnostic info. Tokens persist to `~/.outlook-assistant-tokens.json` and survive server restarts.',
781
+ 'Manage authentication with the Microsoft Graph API. action=`status` (default) returns the current auth state and auto-refreshes the access token if it\'s expired but the refresh token is still valid (~90-day window) — call this first to check before other tools. action=`authenticate` starts the OAuth flow: with `method: "device-code"` (default, works headlessly) it returns a code + URL for the user to visit; with `method: "browser"` it opens the local auth server on :3333 (run `npm run auth-server` first). Pass `force: true` to re-authenticate over an existing valid session. If sign-in reports that OUTLOOK_CLIENT_ID is not configured, ask the user for their Azure Application (client) ID and pass it as `clientId`. action=`device-code-complete` finishes device-code auth after the user enters the code in their browser — call this once authentication shows as successful in the browser. action=`about` returns server version, configured audience, scope list, and other diagnostic info. Tokens persist to `~/.outlook-assistant-tokens.json` and survive server restarts.',
626
782
  annotations: {
627
783
  title: 'Authentication',
628
784
  readOnlyHint: false,
@@ -648,6 +804,11 @@ const authTools = [
648
804
  description:
649
805
  'Force re-authentication even if already authenticated (action=authenticate only)',
650
806
  },
807
+ clientId: {
808
+ type: 'string',
809
+ description:
810
+ "Optional, action=authenticate only. The user's Azure Application (client) ID (a GUID from the app registration's Overview page). Saved to `~/.outlook-assistant-config.json` and used from then on; it is not a secret. The OUTLOOK_CLIENT_ID environment variable takes precedence when set.",
811
+ },
651
812
  },
652
813
  additionalProperties: false,
653
814
  required: [],
package/config.js CHANGED
@@ -14,6 +14,7 @@ const {
14
14
  getFolderFields,
15
15
  } = require('./utils/field-presets');
16
16
  const { VERBOSITY, DEFAULT_LIMITS } = require('./utils/response-formatter');
17
+ const { resolveClientId } = require('./auth/client-config');
17
18
 
18
19
  // Ensure we have a home directory path — never fall back to /tmp (world-readable)
19
20
  const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir();
@@ -165,7 +166,12 @@ module.exports = {
165
166
 
166
167
  // Authentication configuration
167
168
  AUTH_CONFIG: {
168
- clientId: process.env.OUTLOOK_CLIENT_ID || '',
169
+ // Getter, resolved on every read: OUTLOOK_CLIENT_ID → MS_CLIENT_ID →
170
+ // ~/.outlook-assistant-config.json (saved via `auth action=authenticate
171
+ // clientId=…`), so a saved ID takes effect without a restart.
172
+ get clientId() {
173
+ return resolveClientId();
174
+ },
169
175
  clientSecret: process.env.OUTLOOK_CLIENT_SECRET || '',
170
176
  redirectUri: 'http://localhost:3333/auth/callback',
171
177
  // Base scopes, plus the `.Shared` scopes only when OUTLOOK_SHARED_MAILBOX
package/index.js CHANGED
@@ -31,8 +31,10 @@ stdio. It is normally launched by an MCP client (Claude Desktop, Claude Code)
31
31
  rather than run by hand — started from a terminal it will simply wait on stdin.
32
32
 
33
33
  Key environment variables:
34
- OUTLOOK_CLIENT_ID Azure app registration client ID
35
- OUTLOOK_CLIENT_SECRET Client secret VALUE (not the Secret ID)
34
+ OUTLOOK_CLIENT_ID Azure Application (client) ID. If you can't set env
35
+ vars, pass it to the auth tool instead (action=authenticate
36
+ clientId=<id>); it's saved to ~/.outlook-assistant-config.json
37
+ OUTLOOK_CLIENT_SECRET Client secret VALUE (not the Secret ID); browser flow only
36
38
  OUTLOOK_AUTH_METHOD device-code (default) | browser
37
39
  OUTLOOK_AUTH_AUDIENCE common | consumers | organizations | <tenant-guid>
38
40
  OUTLOOK_SHARED_MAILBOX Opt in to shared mailboxes: read | true (work/school only)
package/llms-install.md CHANGED
@@ -11,8 +11,7 @@ Add to your MCP client configuration:
11
11
  "command": "npx",
12
12
  "args": ["-y", "@littlebearapps/outlook-assistant"],
13
13
  "env": {
14
- "OUTLOOK_CLIENT_ID": "<user-must-provide>",
15
- "OUTLOOK_CLIENT_SECRET": "<user-must-provide>"
14
+ "OUTLOOK_CLIENT_ID": "<user-must-provide>"
16
15
  }
17
16
  }
18
17
  }
@@ -36,7 +35,10 @@ Users must create an Azure app registration to get credentials:
36
35
  6. Click "Register"
37
36
  7. Copy the **Application (client) ID** → this is `OUTLOOK_CLIENT_ID`
38
37
 
39
- ### Create a client secret:
38
+ If the client ID is left out of the config, the `auth` tool asks for it at sign-in (`auth action=authenticate clientId=<id>`) and saves it to `~/.outlook-assistant-config.json`.
39
+
40
+ ### Create a client secret (browser flow only):
41
+ The default device-code sign-in doesn't need a secret; skip this unless you'll use `method=browser`, and then add `OUTLOOK_CLIENT_SECRET` to the `env` block.
40
42
  1. Go to "Certificates & secrets" → "New client secret"
41
43
  2. Add a description, select expiration, click "Add"
42
44
  3. **Copy the Value immediately** (not the Secret ID) → this is `OUTLOOK_CLIENT_SECRET`
@@ -69,9 +71,13 @@ Add these to the same `env` block if needed:
69
71
  |----------|---------|
70
72
  | `OUTLOOK_AUTH_AUDIENCE` | `consumers` for Azure apps registered as personal-accounts-only (fixes `AADSTS9002331`); `organizations` or a tenant GUID for work-only apps. Default `common` |
71
73
  | `OUTLOOK_DEFAULT_TIMEZONE` | IANA timezone for calendar times (default `Australia/Melbourne`) |
72
- | `OUTLOOK_MAX_EMAILS_PER_SESSION` | Cap on sends per server session |
74
+ | `OUTLOOK_MAX_EMAILS_PER_SESSION` | Default per-session cap for `send-email`, `draft` and `manage-rules` (override one tool with `OUTLOOK_MAX_<TOOL>_PER_SESSION`, e.g. `OUTLOOK_MAX_SEND_EMAIL_PER_SESSION`) |
73
75
  | `OUTLOOK_ALLOWED_RECIPIENTS` | Comma-separated allowlist of recipient domains/addresses |
74
76
  | `OUTLOOK_SHARED_MAILBOX` | Opt-in shared-mailbox support, work/school accounts only: `read` or `true` (read and organise). Also add `Mail.Read.Shared` (and `Mail.ReadWrite.Shared` for `true`) in Azure, restart, then run `auth` with `action=authenticate` and `force=true` |
77
+ | `OUTLOOK_SEARCH_SCAN_LIMIT` | Messages scanned by the local search fallback on personal accounts (default 500, max 5000) |
78
+ | `OUTLOOK_REQUEST_TIMEOUT_MS` | Per-attempt Graph inactivity timeout in milliseconds (default 60000); not an overall deadline |
79
+
80
+ Run `npx @littlebearapps/outlook-assistant --help` for the full list of environment variables.
75
81
 
76
82
  ## Configuration Files by Client
77
83
 
package/llms.txt CHANGED
@@ -36,7 +36,8 @@ Built by [Little Bear Apps](https://littlebearapps.com).
36
36
  - **MCP safety annotations** on all 22 tools — AI clients auto-approve reads and prompt for destructive operations
37
37
  - **Send-email protections**: pre-send mail tips, dry-run preview, session rate limiting, recipient allowlist
38
38
  - **Rule protections**: dry-run preview on create/update, rate limiting, recipient allowlist on forward/redirect, no permanent-delete action
39
- - **Input and file hardening**: IDs with `.`/`..` path segments refused, the access token only ever sent to `graph.microsoft.com`, attachment downloads and exports confined to the output directory without overwriting
39
+ - **Input and file hardening**: IDs with `.`/`..` path segments refused, the access token only ever sent to `graph.microsoft.com`, attachment downloads and exports (including conversation exports) confined to the output directory without overwriting, and a partly written file removed if a write fails
40
+ - **Throttling-aware Graph client**: `429` (and `503`/`504` for non-POST requests) retried honouring `Retry-After`, a per-attempt inactivity timeout (`OUTLOOK_REQUEST_TIMEOUT_MS`, default 60000 ms) and at most 4 requests in flight, so bulk operations neither hang nor throttle themselves
40
41
  - **Shared mailboxes off by default**: `.Shared` scopes are requested only when `OUTLOOK_SHARED_MAILBOX` is set (`read` keeps shared access read-only)
41
42
  - **Token-optimised**: 22 tools instead of 55 saves ~11,000 tokens per turn (~64% reduction), improving AI accuracy and context efficiency
42
43
  - These safeguards reduce risk but are not foolproof — always review actions before approving
@@ -50,15 +51,14 @@ Built by [Little Bear Apps](https://littlebearapps.com).
50
51
  "command": "npx",
51
52
  "args": ["@littlebearapps/outlook-assistant"],
52
53
  "env": {
53
- "OUTLOOK_CLIENT_ID": "your-application-client-id",
54
- "OUTLOOK_CLIENT_SECRET": "your-client-secret-VALUE"
54
+ "OUTLOOK_CLIENT_ID": "your-application-client-id"
55
55
  }
56
56
  }
57
57
  }
58
58
  }
59
59
  ```
60
60
 
61
- Requires an Azure app registration with Microsoft Graph delegated permissions. See README for full setup.
61
+ `OUTLOOK_CLIENT_SECRET` is only needed for the browser sign-in flow; the default device-code flow uses the client ID alone. Claude Code users can instead install the plugin: `claude plugin marketplace add littlebearapps/outlook-assistant`, then `claude plugin install outlook-assistant@littlebearapps`. Requires an Azure app registration with Microsoft Graph delegated permissions. See README for full setup.
62
62
 
63
63
  ## Tool Categories
64
64
 
@@ -83,6 +83,6 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
83
83
  - [FAQ](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/faq/faq.md): Frequently asked questions — install, accounts, permissions, tokens, send safety, updates, uninstall (also at <https://littlebearapps.com/help/outlook-assistant/faq/>)
84
84
  - [CLAUDE.md](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CLAUDE.md): Quick reference for development
85
85
  - [CONTRIBUTING](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CONTRIBUTING.md): Contribution guidelines
86
- - [CHANGELOG](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CHANGELOG.md): Version history (current: v3.12.1 — Graph reliability and correctness fixes: throttling retries, a request inactivity timeout and a 4-request concurrency cap (#244), complete delta paging (#254), a draft-only guard on `draft` update/send/delete (#246), nested folder paths in `manage-rules` (#248), flag dates honouring `Z` and offsets (#247), attendee types kept on `manage-event update` (#249), no "via API" text on decline/cancel (#242), escaped `$search` phrases (#251), and conversation read/export working on personal accounts. Preceded by v3.12.0 — opt-in shared-mailbox read and organise support via `sharedMailbox` and `OUTLOOK_SHARED_MAILBOX` (#228), `list-events` `startAfter`/`startBefore`/`subject` filters (#193), token refresh requesting the granted scopes plus `offline_access` (#241), and two security fixes: dot segments in resource paths rejected, `export` writes confined to the output directory. Preceded by v3.11.2 — security release: attachment downloads confined to `outputDir` (GHSA-755c-c45g-69rv) and the access token only ever sent to Microsoft Graph (GHSA-mqfm-wfjq-jxq2); v3.11.1 — search and export correctness (filters combined with dates no longer silently dropped, batch export no longer overwrites same-day reply chains); v3.11.0 — `--version`/`--help` CLI flags (#68) and the `AADSTS7000215` Secret ID vs Value explanation (#69); v3.10.0 — search correctness (#217, #229, #230, #231))
87
- - [ROADMAP](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/ROADMAP.md): Active milestones (v3.12.x tool description audit, v3.8.x carry-over, v3.13.0+ new Graph APIs)
86
+ - [CHANGELOG](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CHANGELOG.md): Version history (current: v3.13.0 — marketplace plugin bundle for Claude Code, GitHub Copilot and Cursor; Azure client ID can be given at sign-in and saved locally; client secret documented as browser-flow only)
87
+ - [ROADMAP](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/ROADMAP.md): Active milestones (v3.12.x tool description audit, the patch-release fix queue, v3.8.x carry-over, v3.13.0+ new Graph APIs)
88
88
  - [SECURITY](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/SECURITY.md): Security policy (report vulnerabilities privately via GitHub private vulnerability reporting; acknowledged within 7 days), 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.12.1",
3
+ "version": "3.13.0",
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",
@@ -18,7 +18,8 @@
18
18
  "format": "prettier --write .",
19
19
  "format:check": "prettier --check .",
20
20
  "prepare": "husky || true",
21
- "version": "node -e \"const s=require('./server.json');const p=require('./package.json');s.version=p.version;s.packages[0].version=p.version;require('fs').writeFileSync('./server.json',JSON.stringify(s,null,2)+'\\n')\" && git add server.json"
21
+ "version": "node scripts/sync-version.js && git add server.json plugins/outlook-assistant",
22
+ "version:check": "node scripts/sync-version.js --check"
22
23
  },
23
24
  "lint-staged": {
24
25
  "*.js": [