@softeria/ms-365-mcp-server 0.146.2 → 0.147.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 +14 -0
- package/dist/token-cache-storage.js +77 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -667,6 +667,20 @@ Parent directories are created automatically. Files are written with `0600` perm
|
|
|
667
667
|
|
|
668
668
|
**Without a credential store** (headless Linux, most containers) the key is written to `.cache-key` next to the cache file, with `0600` permissions. That stops the tokens showing up in a stray `cat`, a backup or an accidental commit. It does not protect against anyone who can already read the directory - the key is right there. Use `MS365_MCP_AUTH_CACHE_COMMAND` below if you need the cache in a real secret store.
|
|
669
669
|
|
|
670
|
+
**Skipping the credential store on purpose:**
|
|
671
|
+
|
|
672
|
+
```bash
|
|
673
|
+
export MS365_MCP_USE_KEYTAR=0 # also accepts false, no or off
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
The key then goes to `.cache-key` on every platform, exactly as it does where no credential store exists, and nothing in the server calls keytar. Useful when the credential store prompts on each start - macOS re-asks whenever the calling binary changes, which under `npx` is every version bump - or when the native module misbehaves on your platform rather than simply failing to load. Any other value leaves the credential store in use, and an unrecognised one is warned about rather than passed over silently.
|
|
677
|
+
|
|
678
|
+
Switching it off strands a cache that was encrypted under a key already in the credential store, since nothing can reach that key any more. The server says so and replaces that cache on the next sign-in, which signs out **every** account it held, not just the one you sign back in as. Unset the variable first if that cache is worth keeping.
|
|
679
|
+
|
|
680
|
+
Only a cache that nothing on the machine can open is replaced. One that fails to decrypt while a usable key is sitting right there - a truncated file, a downgrade to an older build, a cache from somewhere else - is damage rather than a stranded cache, and is left alone exactly as it is by default.
|
|
681
|
+
|
|
682
|
+
Two things it deliberately does not do. It never deletes what this server already put in the credential store, on logout or otherwise, because reaching the store is the thing you just asked it to stop doing - clear the `ms-365-mcp-server` entries by hand if you want them gone. And a `.cache-key` that exists but cannot be read (wrong owner on a bind-mounted config directory, say) is treated as recoverable rather than missing: the server refuses both to overwrite a cache and to mint a replacement key, and says so, rather than deleting a key that would work again once the permissions are fixed. Fix the permissions, or delete `.cache-key` yourself to start over - which does mean signing in again.
|
|
683
|
+
|
|
670
684
|
If the cache cannot be decrypted - key lost, keychain locked, file modified - you are asked to sign in again rather than the server failing to start. The cache file is left exactly as it was: not deleted, and not overwritten by that new sign-in either. A keychain that is merely locked usually reads fine on the next start, and the cache is still there when it does.
|
|
671
685
|
|
|
672
686
|
The cost is that the new session is not saved while this lasts, so each start asks you to sign in again. If the key is genuinely gone and the cache will never open, delete `.token-cache.json` to start over - the log says so, and names the path.
|
|
@@ -18,6 +18,7 @@ const TOKEN_CACHE_ACCOUNT = "msal-token-cache";
|
|
|
18
18
|
const SELECTED_ACCOUNT_KEY = "selected-account";
|
|
19
19
|
const AUTH_CACHE_COMMAND_ENV = "MS365_MCP_AUTH_CACHE_COMMAND";
|
|
20
20
|
const AUTH_CACHE_COMMAND_TIMEOUT_ENV = "MS365_MCP_AUTH_CACHE_COMMAND_TIMEOUT_MS";
|
|
21
|
+
const USE_KEYTAR_ENV = "MS365_MCP_USE_KEYTAR";
|
|
21
22
|
const DEFAULT_AUTH_CACHE_COMMAND_TIMEOUT_MS = 1e4;
|
|
22
23
|
const STDERR_LIMIT = 2048;
|
|
23
24
|
const COMMAND_KILL_GRACE_MS = 1e3;
|
|
@@ -28,7 +29,33 @@ const CACHE_KEY_FILE = ".cache-key";
|
|
|
28
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
30
|
const LEGACY_DIR = path.join(path.dirname(__filename), "..");
|
|
30
31
|
let keytar = null;
|
|
32
|
+
let loggedKeytarOptOut = false;
|
|
33
|
+
let warnedKeytarValue = false;
|
|
34
|
+
const KEYTAR_OFF_VALUES = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
35
|
+
const KEYTAR_ON_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
36
|
+
function keytarEnabled() {
|
|
37
|
+
const raw = process.env[USE_KEYTAR_ENV];
|
|
38
|
+
if (raw === void 0) return true;
|
|
39
|
+
const value = raw.trim().toLowerCase();
|
|
40
|
+
if (KEYTAR_OFF_VALUES.has(value)) return false;
|
|
41
|
+
if (value !== "" && !KEYTAR_ON_VALUES.has(value) && !warnedKeytarValue) {
|
|
42
|
+
warnedKeytarValue = true;
|
|
43
|
+
logger.warn(
|
|
44
|
+
`${USE_KEYTAR_ENV} is set to ${JSON.stringify(raw)}, which is not a value this understands, so the system credential store stays in use. Set it to one of ${[...KEYTAR_OFF_VALUES].join(", ")} to turn it off.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
31
49
|
async function getKeytar() {
|
|
50
|
+
if (!keytarEnabled()) {
|
|
51
|
+
if (!loggedKeytarOptOut) {
|
|
52
|
+
loggedKeytarOptOut = true;
|
|
53
|
+
logger.info(
|
|
54
|
+
`${USE_KEYTAR_ENV} is off, using file-based credential storage. Anything this server previously stored under "${SERVICE_NAME}" in the system credential store is now left alone, including on logout, so clear it by hand if you want it gone.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
32
59
|
if (keytar === void 0) {
|
|
33
60
|
return null;
|
|
34
61
|
}
|
|
@@ -172,6 +199,7 @@ function writeFileAtomically(filePath, value) {
|
|
|
172
199
|
}
|
|
173
200
|
let keyStatePromise;
|
|
174
201
|
let encryptionKeyPromise;
|
|
202
|
+
const mintedKeys = [];
|
|
175
203
|
const legacyKeytarEntries = /* @__PURE__ */ new Set();
|
|
176
204
|
const warnedUndecryptable = /* @__PURE__ */ new Set();
|
|
177
205
|
function resetCacheKeyForTests() {
|
|
@@ -180,6 +208,9 @@ function resetCacheKeyForTests() {
|
|
|
180
208
|
legacyKeytarEntries.clear();
|
|
181
209
|
warnedUndecryptable.clear();
|
|
182
210
|
legacyPathsMigrated = false;
|
|
211
|
+
loggedKeytarOptOut = false;
|
|
212
|
+
warnedKeytarValue = false;
|
|
213
|
+
mintedKeys.length = 0;
|
|
183
214
|
}
|
|
184
215
|
async function clearLegacyKeytarEntry(key) {
|
|
185
216
|
if (!legacyKeytarEntries.delete(key)) return;
|
|
@@ -222,16 +253,26 @@ async function readCacheKeys() {
|
|
|
222
253
|
}
|
|
223
254
|
}
|
|
224
255
|
let fileKeyIndex;
|
|
256
|
+
let fileKeyUnreadable = false;
|
|
225
257
|
const keyPath = getCacheKeyPath();
|
|
226
|
-
|
|
258
|
+
let raw;
|
|
259
|
+
try {
|
|
260
|
+
raw = readFileSync(keyPath, "utf8");
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error.code !== "ENOENT") {
|
|
263
|
+
fileKeyUnreadable = true;
|
|
264
|
+
logger.warn(`Could not read the auth cache key file: ${error.message}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (raw !== void 0) {
|
|
227
268
|
try {
|
|
228
|
-
keys.push(parseCacheKey(
|
|
269
|
+
keys.push(parseCacheKey(raw));
|
|
229
270
|
fileKeyIndex = keys.length - 1;
|
|
230
271
|
} catch (error) {
|
|
231
272
|
logger.warn(`Ignoring unusable auth cache key file: ${error.message}`);
|
|
232
273
|
}
|
|
233
274
|
}
|
|
234
|
-
return { keys, fileKeyIndex, canUseKeychain };
|
|
275
|
+
return { keys, fileKeyIndex, canUseKeychain, fileKeyUnreadable };
|
|
235
276
|
}
|
|
236
277
|
function getEncryptionKey() {
|
|
237
278
|
if (!encryptionKeyPromise) {
|
|
@@ -247,7 +288,13 @@ async function resolveEncryptionKey() {
|
|
|
247
288
|
const state = await loadKeyState();
|
|
248
289
|
const preferred = state.keys[state.fileKeyIndex ?? 0];
|
|
249
290
|
if (preferred) return preferred;
|
|
291
|
+
if (state.fileKeyUnreadable) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Refusing to replace the auth cache key at ${getCacheKeyPath()}: it exists but could not be read this run, and minting over it would strand every cache encrypted under it. Fix the permissions on that file, or delete it to start over.`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
250
296
|
const minted = await persistCacheKey(generateCacheKey(), state.canUseKeychain);
|
|
297
|
+
if (!mintedKeys.some((k) => k.equals(minted))) mintedKeys.push(minted);
|
|
251
298
|
state.keys.unshift(minted);
|
|
252
299
|
if (state.fileKeyIndex !== void 0) state.fileKeyIndex += 1;
|
|
253
300
|
return minted;
|
|
@@ -278,8 +325,16 @@ async function persistCacheKey(key, canUseKeychain) {
|
|
|
278
325
|
);
|
|
279
326
|
return key;
|
|
280
327
|
}
|
|
328
|
+
let existingRaw;
|
|
329
|
+
try {
|
|
330
|
+
existingRaw = readFileSync(keyPath, "utf8");
|
|
331
|
+
} catch (readError) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Refusing to replace the auth cache key file at ${keyPath}: it could not be read (${readError.message}), so whether it holds a usable key is unknown.`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
281
336
|
try {
|
|
282
|
-
const existing = parseCacheKey(
|
|
337
|
+
const existing = parseCacheKey(existingRaw);
|
|
283
338
|
logger.info("Another process created the auth cache key file first, adopting it");
|
|
284
339
|
return existing;
|
|
285
340
|
} catch (parseError) {
|
|
@@ -333,12 +388,17 @@ async function readCacheFile(key, cachePath) {
|
|
|
333
388
|
}
|
|
334
389
|
if (onDisk === "") return { status: "absent" };
|
|
335
390
|
if (isEncryptedCache(onDisk)) {
|
|
336
|
-
const
|
|
337
|
-
const decrypted = decryptWithAnyKey(onDisk, keys, key);
|
|
391
|
+
const state = await loadKeyState();
|
|
392
|
+
const decrypted = decryptWithAnyKey(onDisk, state.keys, key);
|
|
338
393
|
if (decrypted !== void 0) return { status: "decrypted", raw: decrypted };
|
|
339
394
|
keyStatePromise = void 0;
|
|
340
395
|
encryptionKeyPromise = void 0;
|
|
341
|
-
|
|
396
|
+
const preExisting = state.keys.filter((k) => !mintedKeys.some((m) => m.equals(k)));
|
|
397
|
+
return {
|
|
398
|
+
status: "unreadable",
|
|
399
|
+
reason: state.fileKeyUnreadable ? "a key file is present but could not be read" : preExisting.length > 0 ? "the keys on hand do not open it" : "no known key opens it",
|
|
400
|
+
noKeyMatched: preExisting.length === 0 && !state.fileKeyUnreadable
|
|
401
|
+
};
|
|
342
402
|
}
|
|
343
403
|
if (isPlainCacheJson(onDisk)) return { status: "plaintext", raw: onDisk };
|
|
344
404
|
return { status: "unreadable", reason: "not a recognisable auth cache" };
|
|
@@ -366,6 +426,15 @@ async function assertOverwritable(key, cachePath) {
|
|
|
366
426
|
warnedUndecryptable.delete(key);
|
|
367
427
|
return;
|
|
368
428
|
}
|
|
429
|
+
if (state.noKeyMatched && !keytarEnabled()) {
|
|
430
|
+
if (!warnedUndecryptable.has(key)) {
|
|
431
|
+
warnedUndecryptable.add(key);
|
|
432
|
+
logger.warn(
|
|
433
|
+
`Replacing ${cachePath}: there is no auth cache key on this machine to open it with, and ${USE_KEYTAR_ENV} is off, so the key it was written under is most likely the one in the system credential store. Signing in again rewrites it against the key file instead. If that cache is worth keeping, stop the server and unset ${USE_KEYTAR_ENV} before signing in.`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
369
438
|
if (!warnedUndecryptable.has(key)) {
|
|
370
439
|
warnedUndecryptable.add(key);
|
|
371
440
|
logger.warn(
|
|
@@ -614,6 +683,7 @@ export {
|
|
|
614
683
|
getCacheKeyPath,
|
|
615
684
|
getSelectedAccountPath,
|
|
616
685
|
getTokenCachePath,
|
|
686
|
+
keytarEnabled,
|
|
617
687
|
migrateLegacyPathsFrom,
|
|
618
688
|
pickNewest,
|
|
619
689
|
resetCacheKeyForTests,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
3
|
"mcpName": "io.github.Softeria/ms-365-mcp-server",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.147.0",
|
|
5
5
|
"description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|