@softeria/ms-365-mcp-server 0.146.2 → 0.148.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/endpoints.json +32 -0
- package/dist/generated/client.js +66 -0
- package/dist/token-cache-storage.js +77 -7
- package/package.json +1 -1
- package/src/endpoints.json +32 -0
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.
|
package/dist/endpoints.json
CHANGED
|
@@ -165,6 +165,38 @@
|
|
|
165
165
|
"workScopes": ["Mail.Send.Shared"],
|
|
166
166
|
"llmTip": "Forward a message from a shared mailbox preserving full HTML formatting and attachments. The 'user-id' is the shared mailbox email address. 'toRecipients' is required. The 'comment' field adds text above the forwarded content. Requires Send As permission on the shared mailbox in Exchange."
|
|
167
167
|
},
|
|
168
|
+
{
|
|
169
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/createReply",
|
|
170
|
+
"method": "post",
|
|
171
|
+
"toolName": "create-shared-mailbox-reply-draft",
|
|
172
|
+
"presets": ["work"],
|
|
173
|
+
"workScopes": ["Mail.ReadWrite.Shared"],
|
|
174
|
+
"llmTip": "Create a reply draft in a shared mailbox preserving thread metadata (conversationId, In-Reply-To, References). The 'user-id' is the shared mailbox email address (e.g. support@contoso.com). Does not send — use send-shared-mailbox-draft to send afterwards. For HTML replies pass Message.body.contentType: 'html' with Message.body.content as HTML. Specifying both 'comment' and Message.body returns 400. Requires delegated access to the shared mailbox."
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/createReplyAll",
|
|
178
|
+
"method": "post",
|
|
179
|
+
"toolName": "create-shared-mailbox-reply-all-draft",
|
|
180
|
+
"presets": ["work"],
|
|
181
|
+
"workScopes": ["Mail.ReadWrite.Shared"],
|
|
182
|
+
"llmTip": "Create a reply-all draft in a shared mailbox preserving thread metadata (conversationId, In-Reply-To, References). The 'user-id' is the shared mailbox email address. Does not send — use send-shared-mailbox-draft to send afterwards. For HTML replies pass Message.body.contentType: 'html' with Message.body.content as HTML. Specifying both 'comment' and Message.body returns 400. Requires delegated access to the shared mailbox."
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/createForward",
|
|
186
|
+
"method": "post",
|
|
187
|
+
"toolName": "create-shared-mailbox-forward-draft",
|
|
188
|
+
"presets": ["work"],
|
|
189
|
+
"workScopes": ["Mail.ReadWrite.Shared"],
|
|
190
|
+
"llmTip": "Create a forward draft in a shared mailbox without sending. The 'user-id' is the shared mailbox email address. Does not send — use send-shared-mailbox-draft to send afterwards. Useful when the user wants to review before the forward goes out. Requires delegated access to the shared mailbox."
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/send",
|
|
194
|
+
"method": "post",
|
|
195
|
+
"toolName": "send-shared-mailbox-draft",
|
|
196
|
+
"presets": ["work"],
|
|
197
|
+
"workScopes": ["Mail.Send.Shared"],
|
|
198
|
+
"llmTip": "Send an existing draft from a shared mailbox. No request body needed — just call with the message ID and the shared mailbox user-id. Draft must exist in the shared mailbox's Drafts folder. Requires Send As permission on the shared mailbox in Exchange."
|
|
199
|
+
},
|
|
168
200
|
{
|
|
169
201
|
"pathPattern": "/users",
|
|
170
202
|
"method": "get",
|
package/dist/generated/client.js
CHANGED
|
@@ -15252,6 +15252,64 @@ Only the policyViolation property of a chatMessage can be updated in application
|
|
|
15252
15252
|
],
|
|
15253
15253
|
response: z.void()
|
|
15254
15254
|
},
|
|
15255
|
+
{
|
|
15256
|
+
method: "post",
|
|
15257
|
+
path: "/users/:userId/messages/:messageId/createForward",
|
|
15258
|
+
alias: "create-shared-mailbox-forward-draft",
|
|
15259
|
+
description: `Create a shared mailbox forward draft.`,
|
|
15260
|
+
requestFormat: "json",
|
|
15261
|
+
parameters: [
|
|
15262
|
+
{
|
|
15263
|
+
name: "body",
|
|
15264
|
+
description: `Action parameters`,
|
|
15265
|
+
type: "Body",
|
|
15266
|
+
schema: create_forward_draft_Body
|
|
15267
|
+
}
|
|
15268
|
+
],
|
|
15269
|
+
response: z.void()
|
|
15270
|
+
},
|
|
15271
|
+
{
|
|
15272
|
+
method: "post",
|
|
15273
|
+
path: "/users/:userId/messages/:messageId/createReply",
|
|
15274
|
+
alias: "create-shared-mailbox-reply-draft",
|
|
15275
|
+
description: `Create a draft to reply to the sender of a message in either JSON or MIME format. When using JSON format:
|
|
15276
|
+
- Specify either a comment or the body property of the message parameter. Specifying both will return an HTTP 400 Bad Request error.
|
|
15277
|
+
- If replyTo is specified in the original message, per Internet Message Format (RFC 2822), you should send the reply to the recipients in replyTo, and not the recipients in from.
|
|
15278
|
+
- You can update the draft later to add reply content to the body or change other message properties. When using MIME format:
|
|
15279
|
+
- Provide the applicable Internet message headers and the MIME content, all encoded in base64 format in the request body.
|
|
15280
|
+
- Add any attachments and S/MIME properties to the MIME content. Send the draft message in a subsequent operation. Alternatively, reply to a message in a single operation.`,
|
|
15281
|
+
requestFormat: "json",
|
|
15282
|
+
parameters: [
|
|
15283
|
+
{
|
|
15284
|
+
name: "body",
|
|
15285
|
+
description: `Action parameters`,
|
|
15286
|
+
type: "Body",
|
|
15287
|
+
schema: create_reply_draft_Body
|
|
15288
|
+
}
|
|
15289
|
+
],
|
|
15290
|
+
response: z.void()
|
|
15291
|
+
},
|
|
15292
|
+
{
|
|
15293
|
+
method: "post",
|
|
15294
|
+
path: "/users/:userId/messages/:messageId/createReplyAll",
|
|
15295
|
+
alias: "create-shared-mailbox-reply-all-draft",
|
|
15296
|
+
description: `Create a draft to reply to the sender and all recipients of a message in either JSON or MIME format. When using JSON format:
|
|
15297
|
+
- Specify either a comment or the body property of the message parameter. Specifying both will return an HTTP 400 Bad Request error.
|
|
15298
|
+
- If the original message specifies a recipient in the replyTo property, per Internet Message Format (RFC 2822), you should send the reply to the recipients in the replyTo and toRecipients properties, and not the recipients in the from and toRecipients properties.
|
|
15299
|
+
- You can update the draft later to add reply content to the body or change other message properties. When using MIME format:
|
|
15300
|
+
- Provide the applicable Internet message headers and the MIME content, all encoded in base64 format in the request body.
|
|
15301
|
+
- Add any attachments and S/MIME properties to the MIME content. Send the draft message in a subsequent operation. Alternatively, reply-all to a message in a single action.`,
|
|
15302
|
+
requestFormat: "json",
|
|
15303
|
+
parameters: [
|
|
15304
|
+
{
|
|
15305
|
+
name: "body",
|
|
15306
|
+
description: `Action parameters`,
|
|
15307
|
+
type: "Body",
|
|
15308
|
+
schema: create_reply_draft_Body
|
|
15309
|
+
}
|
|
15310
|
+
],
|
|
15311
|
+
response: z.void()
|
|
15312
|
+
},
|
|
15255
15313
|
{
|
|
15256
15314
|
method: "post",
|
|
15257
15315
|
path: "/users/:userId/messages/:messageId/forward",
|
|
@@ -15328,6 +15386,14 @@ Only the policyViolation property of a chatMessage can be updated in application
|
|
|
15328
15386
|
],
|
|
15329
15387
|
response: z.void()
|
|
15330
15388
|
},
|
|
15389
|
+
{
|
|
15390
|
+
method: "post",
|
|
15391
|
+
path: "/users/:userId/messages/:messageId/send",
|
|
15392
|
+
alias: "send-shared-mailbox-draft",
|
|
15393
|
+
description: `Send an existing draft message. The draft message can be a new message draft, reply draft, reply-all draft, or a forward draft. This method saves the message in the Sent Items folder. Alternatively, send a new message in a single operation.`,
|
|
15394
|
+
requestFormat: "json",
|
|
15395
|
+
response: z.void()
|
|
15396
|
+
},
|
|
15331
15397
|
{
|
|
15332
15398
|
method: "get",
|
|
15333
15399
|
path: "/users/:userId/presence",
|
|
@@ -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.148.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",
|
package/src/endpoints.json
CHANGED
|
@@ -165,6 +165,38 @@
|
|
|
165
165
|
"workScopes": ["Mail.Send.Shared"],
|
|
166
166
|
"llmTip": "Forward a message from a shared mailbox preserving full HTML formatting and attachments. The 'user-id' is the shared mailbox email address. 'toRecipients' is required. The 'comment' field adds text above the forwarded content. Requires Send As permission on the shared mailbox in Exchange."
|
|
167
167
|
},
|
|
168
|
+
{
|
|
169
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/createReply",
|
|
170
|
+
"method": "post",
|
|
171
|
+
"toolName": "create-shared-mailbox-reply-draft",
|
|
172
|
+
"presets": ["work"],
|
|
173
|
+
"workScopes": ["Mail.ReadWrite.Shared"],
|
|
174
|
+
"llmTip": "Create a reply draft in a shared mailbox preserving thread metadata (conversationId, In-Reply-To, References). The 'user-id' is the shared mailbox email address (e.g. support@contoso.com). Does not send — use send-shared-mailbox-draft to send afterwards. For HTML replies pass Message.body.contentType: 'html' with Message.body.content as HTML. Specifying both 'comment' and Message.body returns 400. Requires delegated access to the shared mailbox."
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/createReplyAll",
|
|
178
|
+
"method": "post",
|
|
179
|
+
"toolName": "create-shared-mailbox-reply-all-draft",
|
|
180
|
+
"presets": ["work"],
|
|
181
|
+
"workScopes": ["Mail.ReadWrite.Shared"],
|
|
182
|
+
"llmTip": "Create a reply-all draft in a shared mailbox preserving thread metadata (conversationId, In-Reply-To, References). The 'user-id' is the shared mailbox email address. Does not send — use send-shared-mailbox-draft to send afterwards. For HTML replies pass Message.body.contentType: 'html' with Message.body.content as HTML. Specifying both 'comment' and Message.body returns 400. Requires delegated access to the shared mailbox."
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/createForward",
|
|
186
|
+
"method": "post",
|
|
187
|
+
"toolName": "create-shared-mailbox-forward-draft",
|
|
188
|
+
"presets": ["work"],
|
|
189
|
+
"workScopes": ["Mail.ReadWrite.Shared"],
|
|
190
|
+
"llmTip": "Create a forward draft in a shared mailbox without sending. The 'user-id' is the shared mailbox email address. Does not send — use send-shared-mailbox-draft to send afterwards. Useful when the user wants to review before the forward goes out. Requires delegated access to the shared mailbox."
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/send",
|
|
194
|
+
"method": "post",
|
|
195
|
+
"toolName": "send-shared-mailbox-draft",
|
|
196
|
+
"presets": ["work"],
|
|
197
|
+
"workScopes": ["Mail.Send.Shared"],
|
|
198
|
+
"llmTip": "Send an existing draft from a shared mailbox. No request body needed — just call with the message ID and the shared mailbox user-id. Draft must exist in the shared mailbox's Drafts folder. Requires Send As permission on the shared mailbox in Exchange."
|
|
199
|
+
},
|
|
168
200
|
{
|
|
169
201
|
"pathPattern": "/users",
|
|
170
202
|
"method": "get",
|