@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 +51 -16
- package/auth/client-config.js +142 -0
- package/auth/index.js +4 -2
- package/auth/oauth-server.js +5 -1
- package/auth/token-storage.js +29 -14
- package/auth/tools.js +179 -18
- package/config.js +7 -1
- package/index.js +4 -2
- package/llms-install.md +10 -4
- package/llms.txt +6 -6
- package/package.json +3 -2
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.
|
|
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
|
-
|
|
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
|
-
[](cursor://anysphere.cursor-deeplink/mcp/install?name=Outlook%20Assistant&config=
|
|
263
|
+
[](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,
|
package/auth/oauth-server.js
CHANGED
|
@@ -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`] ||
|
|
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 ||
|
package/auth/token-storage.js
CHANGED
|
@@ -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:
|
|
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.
|
|
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.
|
|
164
|
+
console.error('No access token available.');
|
|
153
165
|
return null;
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
if (this.isTokenExpired()) {
|
|
157
|
-
console.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
321
|
+
console.error('Exchanging authorization code for tokens...');
|
|
307
322
|
const requestedScopes = this.config.scopes;
|
|
308
323
|
const postData = querystring.stringify({
|
|
309
|
-
client_id:
|
|
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.
|
|
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.
|
|
426
|
+
console.error('Token file deleted successfully.');
|
|
412
427
|
} catch (error) {
|
|
413
428
|
if (error.code === 'ENOENT') {
|
|
414
|
-
console.
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
35
|
-
|
|
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
|
-
|
|
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` |
|
|
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.
|
|
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.
|
|
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 -
|
|
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": [
|