@softeria/ms-365-mcp-server 0.128.1 → 0.129.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 +1 -1
- package/dist/__tests__/graph-tools.test.js +253 -4
- package/dist/auth.js +43 -18
- package/dist/cli.js +3 -2
- package/dist/endpoints.json +4 -0
- package/dist/generated/client.js +34 -32
- package/dist/graph-tools.js +34 -6
- package/dist/lib/destructive-ops.js +9 -0
- package/dist/lib/tool-schema.js +13 -2
- package/docs/deployment.md +2 -0
- package/package.json +1 -1
- package/src/endpoints.json +4 -0
package/README.md
CHANGED
|
@@ -297,7 +297,7 @@ Open WebUI supports MCP servers via HTTP transport with OAuth 2.1.
|
|
|
297
297
|
|
|
298
298
|
3. Click **Register Client**.
|
|
299
299
|
|
|
300
|
-
> **Note**: Dynamic client registration is enabled by default in HTTP mode. Use `--no-dynamic-registration` to disable it. If using a custom Azure Entra app, the platform type for your redirect URI depends on whether the app has a client secret: with a secret use "Web", without one use "Mobile and desktop applications" (never "Single-page application").
|
|
300
|
+
> **Note**: Dynamic client registration is enabled by default in HTTP mode. Use `--no-dynamic-registration` (or set `MS365_MCP_DISABLE_DCR=true`) to disable it. If using a custom Azure Entra app, the platform type for your redirect URI depends on whether the app has a client secret: with a secret use "Web", without one use "Mobile and desktop applications" (never "Single-page application").
|
|
301
301
|
|
|
302
302
|
**Quick test setup** using the default Azure app (ID `ms-365` and `localhost:8080` are pre-configured):
|
|
303
303
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
vi.mock("../logger.js", () => ({
|
|
4
4
|
default: {
|
|
@@ -530,7 +530,9 @@ describe("graph-tools", () => {
|
|
|
530
530
|
registerGraphTools(server, graphClient);
|
|
531
531
|
await server.tools.get("create-reply-draft").handler({
|
|
532
532
|
messageId: "AAMk123",
|
|
533
|
-
body: { Message: { body: { contentType: "html", content: "<p>hi</p>" } } }
|
|
533
|
+
body: { Message: { body: { contentType: "html", content: "<p>hi</p>" } } },
|
|
534
|
+
confirm: true
|
|
535
|
+
// destructive POST — required by isDestructiveOperation gate
|
|
534
536
|
});
|
|
535
537
|
const [, options] = graphClient.graphRequest.mock.calls[0];
|
|
536
538
|
const prefer = options.headers["Prefer"];
|
|
@@ -571,7 +573,9 @@ describe("graph-tools", () => {
|
|
|
571
573
|
await server.tools.get("upload-file-content").handler({
|
|
572
574
|
driveId: "drive123",
|
|
573
575
|
driveItemId: "item456",
|
|
574
|
-
body: base64
|
|
576
|
+
body: base64,
|
|
577
|
+
confirm: true
|
|
578
|
+
// destructive PUT — required by isDestructiveOperation gate
|
|
575
579
|
});
|
|
576
580
|
const [path, options] = graphClient.graphRequest.mock.calls[0];
|
|
577
581
|
expect(path).toBe("/drives/drive123/items/item456/content");
|
|
@@ -607,7 +611,9 @@ describe("graph-tools", () => {
|
|
|
607
611
|
await server.tools.get("upload-file-content").handler({
|
|
608
612
|
driveId: "d",
|
|
609
613
|
driveItemId: "i",
|
|
610
|
-
body: Buffer.from("%PDF-1.4").toString("base64")
|
|
614
|
+
body: Buffer.from("%PDF-1.4").toString("base64"),
|
|
615
|
+
confirm: true
|
|
616
|
+
// destructive PUT — required by isDestructiveOperation gate
|
|
611
617
|
});
|
|
612
618
|
const [, options] = graphClient.graphRequest.mock.calls[0];
|
|
613
619
|
expect(options.headers["Content-Type"]).toBe("application/pdf");
|
|
@@ -1267,4 +1273,247 @@ describe("graph-tools", () => {
|
|
|
1267
1273
|
expect(server.tools.has("parse-teams-url")).toBe(true);
|
|
1268
1274
|
});
|
|
1269
1275
|
});
|
|
1276
|
+
describe("destructive operations require confirm: true", () => {
|
|
1277
|
+
const prevRequireConfirm = process.env.MS365_MCP_REQUIRE_CONFIRM;
|
|
1278
|
+
beforeEach(() => {
|
|
1279
|
+
process.env.MS365_MCP_REQUIRE_CONFIRM = "true";
|
|
1280
|
+
});
|
|
1281
|
+
afterEach(() => {
|
|
1282
|
+
if (prevRequireConfirm === void 0) delete process.env.MS365_MCP_REQUIRE_CONFIRM;
|
|
1283
|
+
else process.env.MS365_MCP_REQUIRE_CONFIRM = prevRequireConfirm;
|
|
1284
|
+
});
|
|
1285
|
+
it("rejects DELETE without confirm: true and does NOT call Graph", async () => {
|
|
1286
|
+
const endpoint = makeEndpoint({
|
|
1287
|
+
method: "delete",
|
|
1288
|
+
path: "/me/messages/:message-id",
|
|
1289
|
+
alias: "delete-mail-message"
|
|
1290
|
+
});
|
|
1291
|
+
const config = makeConfig({
|
|
1292
|
+
pathPattern: "/me/messages/{message-id}",
|
|
1293
|
+
method: "delete",
|
|
1294
|
+
toolName: "delete-mail-message"
|
|
1295
|
+
});
|
|
1296
|
+
mockEndpoints.push(endpoint);
|
|
1297
|
+
mockEndpointsJson = [config];
|
|
1298
|
+
const graphClient = createMockGraphClient();
|
|
1299
|
+
const server = createMockServer();
|
|
1300
|
+
const { registerGraphTools } = await loadModule();
|
|
1301
|
+
registerGraphTools(server, graphClient);
|
|
1302
|
+
const tool = server.tools.get("delete-mail-message");
|
|
1303
|
+
const result = await tool.handler({ messageId: "abc" });
|
|
1304
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
1305
|
+
expect(result.isError).toBe(true);
|
|
1306
|
+
const payload = JSON.parse(result.content[0].text);
|
|
1307
|
+
expect(payload.error).toBe("confirmation_required");
|
|
1308
|
+
expect(payload.tool).toBe("delete-mail-message");
|
|
1309
|
+
expect(payload.destructive).toBe(true);
|
|
1310
|
+
});
|
|
1311
|
+
it("allows DELETE when confirm: true is passed", async () => {
|
|
1312
|
+
const endpoint = makeEndpoint({
|
|
1313
|
+
method: "delete",
|
|
1314
|
+
path: "/me/messages/:message-id",
|
|
1315
|
+
alias: "delete-mail-message"
|
|
1316
|
+
});
|
|
1317
|
+
const config = makeConfig({
|
|
1318
|
+
pathPattern: "/me/messages/{message-id}",
|
|
1319
|
+
method: "delete",
|
|
1320
|
+
toolName: "delete-mail-message"
|
|
1321
|
+
});
|
|
1322
|
+
mockEndpoints.push(endpoint);
|
|
1323
|
+
mockEndpointsJson = [config];
|
|
1324
|
+
const graphClient = createMockGraphClient([
|
|
1325
|
+
{ content: [{ type: "text", text: JSON.stringify({ status: 204 }) }] }
|
|
1326
|
+
]);
|
|
1327
|
+
const server = createMockServer();
|
|
1328
|
+
const { registerGraphTools } = await loadModule();
|
|
1329
|
+
registerGraphTools(server, graphClient);
|
|
1330
|
+
const tool = server.tools.get("delete-mail-message");
|
|
1331
|
+
await tool.handler({ messageId: "abc", confirm: true });
|
|
1332
|
+
expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
|
|
1333
|
+
});
|
|
1334
|
+
it("does NOT send `confirm` to Graph as a query/body parameter", async () => {
|
|
1335
|
+
const endpoint = makeEndpoint({
|
|
1336
|
+
method: "post",
|
|
1337
|
+
path: "/me/sendMail",
|
|
1338
|
+
alias: "send-mail",
|
|
1339
|
+
parameters: [{ name: "message", type: "Body", schema: z.any() }]
|
|
1340
|
+
});
|
|
1341
|
+
const config = makeConfig({
|
|
1342
|
+
pathPattern: "/me/sendMail",
|
|
1343
|
+
method: "post",
|
|
1344
|
+
toolName: "send-mail"
|
|
1345
|
+
});
|
|
1346
|
+
mockEndpoints.push(endpoint);
|
|
1347
|
+
mockEndpointsJson = [config];
|
|
1348
|
+
const graphClient = createMockGraphClient([
|
|
1349
|
+
{ content: [{ type: "text", text: JSON.stringify({ status: 202 }) }] }
|
|
1350
|
+
]);
|
|
1351
|
+
const server = createMockServer();
|
|
1352
|
+
const { registerGraphTools } = await loadModule();
|
|
1353
|
+
registerGraphTools(server, graphClient);
|
|
1354
|
+
const tool = server.tools.get("send-mail");
|
|
1355
|
+
await tool.handler({ message: { subject: "hi" }, confirm: true });
|
|
1356
|
+
const [url, opts] = graphClient.graphRequest.mock.calls[0];
|
|
1357
|
+
expect(url).not.toContain("confirm");
|
|
1358
|
+
const body = JSON.parse(opts.body);
|
|
1359
|
+
expect(body).not.toHaveProperty("confirm");
|
|
1360
|
+
});
|
|
1361
|
+
it("allows GET (read-only) regardless of confirm", async () => {
|
|
1362
|
+
const endpoint = makeEndpoint();
|
|
1363
|
+
const config = makeConfig();
|
|
1364
|
+
mockEndpoints.push(endpoint);
|
|
1365
|
+
mockEndpointsJson = [config];
|
|
1366
|
+
const graphClient = createMockGraphClient([
|
|
1367
|
+
{ content: [{ type: "text", text: JSON.stringify({ value: [] }) }] }
|
|
1368
|
+
]);
|
|
1369
|
+
const server = createMockServer();
|
|
1370
|
+
const { registerGraphTools } = await loadModule();
|
|
1371
|
+
registerGraphTools(server, graphClient);
|
|
1372
|
+
const tool = server.tools.get("test-tool");
|
|
1373
|
+
await tool.handler({});
|
|
1374
|
+
expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
|
|
1375
|
+
});
|
|
1376
|
+
it("allows POST endpoints flagged readOnly without confirm (e.g. find-meeting-times)", async () => {
|
|
1377
|
+
const endpoint = makeEndpoint({
|
|
1378
|
+
method: "post",
|
|
1379
|
+
path: "/me/findMeetingTimes",
|
|
1380
|
+
alias: "find-meeting-times"
|
|
1381
|
+
});
|
|
1382
|
+
const config = makeConfig({
|
|
1383
|
+
pathPattern: "/me/findMeetingTimes",
|
|
1384
|
+
method: "post",
|
|
1385
|
+
toolName: "find-meeting-times",
|
|
1386
|
+
readOnly: true
|
|
1387
|
+
});
|
|
1388
|
+
mockEndpoints.push(endpoint);
|
|
1389
|
+
mockEndpointsJson = [config];
|
|
1390
|
+
const graphClient = createMockGraphClient([
|
|
1391
|
+
{ content: [{ type: "text", text: JSON.stringify({ meetingTimeSuggestions: [] }) }] }
|
|
1392
|
+
]);
|
|
1393
|
+
const server = createMockServer();
|
|
1394
|
+
const { registerGraphTools } = await loadModule();
|
|
1395
|
+
registerGraphTools(server, graphClient);
|
|
1396
|
+
const tool = server.tools.get("find-meeting-times");
|
|
1397
|
+
await tool.handler({});
|
|
1398
|
+
expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
|
|
1399
|
+
});
|
|
1400
|
+
it("does NOT gate when MS365_MCP_REQUIRE_CONFIRM is unset (opt-in, off by default)", async () => {
|
|
1401
|
+
delete process.env.MS365_MCP_REQUIRE_CONFIRM;
|
|
1402
|
+
const endpoint = makeEndpoint({
|
|
1403
|
+
method: "delete",
|
|
1404
|
+
path: "/me/messages/:message-id",
|
|
1405
|
+
alias: "delete-mail-message"
|
|
1406
|
+
});
|
|
1407
|
+
const config = makeConfig({
|
|
1408
|
+
pathPattern: "/me/messages/{message-id}",
|
|
1409
|
+
method: "delete",
|
|
1410
|
+
toolName: "delete-mail-message"
|
|
1411
|
+
});
|
|
1412
|
+
mockEndpoints.push(endpoint);
|
|
1413
|
+
mockEndpointsJson = [config];
|
|
1414
|
+
const graphClient = createMockGraphClient([
|
|
1415
|
+
{ content: [{ type: "text", text: JSON.stringify({ status: 204 }) }] }
|
|
1416
|
+
]);
|
|
1417
|
+
const server = createMockServer();
|
|
1418
|
+
const { registerGraphTools } = await loadModule();
|
|
1419
|
+
registerGraphTools(server, graphClient);
|
|
1420
|
+
const tool = server.tools.get("delete-mail-message");
|
|
1421
|
+
await tool.handler({ messageId: "abc" });
|
|
1422
|
+
expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
|
|
1423
|
+
});
|
|
1424
|
+
it("does NOT gate when MS365_MCP_REQUIRE_CONFIRM=false (explicit off)", async () => {
|
|
1425
|
+
process.env.MS365_MCP_REQUIRE_CONFIRM = "false";
|
|
1426
|
+
const endpoint = makeEndpoint({
|
|
1427
|
+
method: "delete",
|
|
1428
|
+
path: "/me/messages/:message-id",
|
|
1429
|
+
alias: "delete-mail-message"
|
|
1430
|
+
});
|
|
1431
|
+
const config = makeConfig({
|
|
1432
|
+
pathPattern: "/me/messages/{message-id}",
|
|
1433
|
+
method: "delete",
|
|
1434
|
+
toolName: "delete-mail-message"
|
|
1435
|
+
});
|
|
1436
|
+
mockEndpoints.push(endpoint);
|
|
1437
|
+
mockEndpointsJson = [config];
|
|
1438
|
+
const graphClient = createMockGraphClient([
|
|
1439
|
+
{ content: [{ type: "text", text: JSON.stringify({ status: 204 }) }] }
|
|
1440
|
+
]);
|
|
1441
|
+
const server = createMockServer();
|
|
1442
|
+
const { registerGraphTools } = await loadModule();
|
|
1443
|
+
registerGraphTools(server, graphClient);
|
|
1444
|
+
const tool = server.tools.get("delete-mail-message");
|
|
1445
|
+
await tool.handler({ messageId: "abc" });
|
|
1446
|
+
expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
|
|
1447
|
+
});
|
|
1448
|
+
it("rejects `confirm: false` as not equal to true", async () => {
|
|
1449
|
+
const endpoint = makeEndpoint({
|
|
1450
|
+
method: "patch",
|
|
1451
|
+
path: "/me/messages/:message-id",
|
|
1452
|
+
alias: "update-mail-message"
|
|
1453
|
+
});
|
|
1454
|
+
const config = makeConfig({
|
|
1455
|
+
pathPattern: "/me/messages/{message-id}",
|
|
1456
|
+
method: "patch",
|
|
1457
|
+
toolName: "update-mail-message"
|
|
1458
|
+
});
|
|
1459
|
+
mockEndpoints.push(endpoint);
|
|
1460
|
+
mockEndpointsJson = [config];
|
|
1461
|
+
const graphClient = createMockGraphClient();
|
|
1462
|
+
const server = createMockServer();
|
|
1463
|
+
const { registerGraphTools } = await loadModule();
|
|
1464
|
+
registerGraphTools(server, graphClient);
|
|
1465
|
+
const tool = server.tools.get("update-mail-message");
|
|
1466
|
+
const result = await tool.handler({ messageId: "abc", confirm: false });
|
|
1467
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
1468
|
+
expect(result.isError).toBe(true);
|
|
1469
|
+
});
|
|
1470
|
+
it("exposes confirm in the schema only for destructive tools", async () => {
|
|
1471
|
+
mockEndpoints.push(makeEndpoint());
|
|
1472
|
+
mockEndpointsJson = [makeConfig()];
|
|
1473
|
+
mockEndpoints.push(
|
|
1474
|
+
makeEndpoint({ method: "delete", alias: "destructive-tool", path: "/me/items/:item-id" })
|
|
1475
|
+
);
|
|
1476
|
+
mockEndpointsJson.push(
|
|
1477
|
+
makeConfig({
|
|
1478
|
+
method: "delete",
|
|
1479
|
+
toolName: "destructive-tool",
|
|
1480
|
+
pathPattern: "/me/items/{item-id}"
|
|
1481
|
+
})
|
|
1482
|
+
);
|
|
1483
|
+
const server = createMockServer();
|
|
1484
|
+
const { registerGraphTools } = await loadModule();
|
|
1485
|
+
registerGraphTools(server, createMockGraphClient());
|
|
1486
|
+
expect(server.tools.get("test-tool").schema).not.toHaveProperty("confirm");
|
|
1487
|
+
expect(server.tools.get("destructive-tool").schema).toHaveProperty("confirm");
|
|
1488
|
+
});
|
|
1489
|
+
});
|
|
1490
|
+
describe("isDestructiveOperation", () => {
|
|
1491
|
+
it("returns true for POST, PATCH, PUT, DELETE", async () => {
|
|
1492
|
+
const { isDestructiveOperation } = await loadModule();
|
|
1493
|
+
expect(isDestructiveOperation("POST", void 0)).toBe(true);
|
|
1494
|
+
expect(isDestructiveOperation("PATCH", void 0)).toBe(true);
|
|
1495
|
+
expect(isDestructiveOperation("PUT", void 0)).toBe(true);
|
|
1496
|
+
expect(isDestructiveOperation("DELETE", void 0)).toBe(true);
|
|
1497
|
+
});
|
|
1498
|
+
it("is case-insensitive", async () => {
|
|
1499
|
+
const { isDestructiveOperation } = await loadModule();
|
|
1500
|
+
expect(isDestructiveOperation("delete", void 0)).toBe(true);
|
|
1501
|
+
expect(isDestructiveOperation("Patch", void 0)).toBe(true);
|
|
1502
|
+
});
|
|
1503
|
+
it("returns false for GET / HEAD / OPTIONS", async () => {
|
|
1504
|
+
const { isDestructiveOperation } = await loadModule();
|
|
1505
|
+
expect(isDestructiveOperation("GET", void 0)).toBe(false);
|
|
1506
|
+
expect(isDestructiveOperation("HEAD", void 0)).toBe(false);
|
|
1507
|
+
expect(isDestructiveOperation("OPTIONS", void 0)).toBe(false);
|
|
1508
|
+
});
|
|
1509
|
+
it("returns false for POST endpoints flagged readOnly", async () => {
|
|
1510
|
+
const { isDestructiveOperation } = await loadModule();
|
|
1511
|
+
expect(isDestructiveOperation("POST", { readOnly: true })).toBe(false);
|
|
1512
|
+
});
|
|
1513
|
+
it("still returns true for PATCH/DELETE even if config.readOnly is set (should not happen but defensive)", async () => {
|
|
1514
|
+
const { isDestructiveOperation } = await loadModule();
|
|
1515
|
+
expect(isDestructiveOperation("PATCH", { readOnly: true })).toBe(true);
|
|
1516
|
+
expect(isDestructiveOperation("DELETE", { readOnly: true })).toBe(true);
|
|
1517
|
+
});
|
|
1518
|
+
});
|
|
1270
1519
|
});
|
package/dist/auth.js
CHANGED
|
@@ -31,6 +31,36 @@ function createMsalConfig(secrets) {
|
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
function buildDiskCoherencyCachePlugin(storage) {
|
|
35
|
+
return {
|
|
36
|
+
beforeCacheAccess: async (context) => {
|
|
37
|
+
try {
|
|
38
|
+
const cacheRaw = await storage.load("token-cache");
|
|
39
|
+
if (cacheRaw) {
|
|
40
|
+
context.tokenCache.deserialize(unwrapCache(cacheRaw).data);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logger.error(`Error reloading token cache: ${error.message}`);
|
|
44
|
+
if (storage.failClosed) {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
afterCacheAccess: async (context) => {
|
|
50
|
+
if (!context.cacheHasChanged) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await storage.save("token-cache", wrapCache(context.tokenCache.serialize()));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.error(`Error saving token cache: ${error.message}`);
|
|
57
|
+
if (storage.failClosed) {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
34
64
|
const SCOPE_HIERARCHY = {
|
|
35
65
|
"Mail.ReadWrite": ["Mail.Read"],
|
|
36
66
|
"Calendars.ReadWrite": ["Calendars.Read"],
|
|
@@ -312,8 +342,15 @@ function consumersAuthorityHint(error, account, authority) {
|
|
|
312
342
|
class AuthManager {
|
|
313
343
|
constructor(config, scopes = [], expectedAccount, storage) {
|
|
314
344
|
logger.info(`And scopes are ${scopes.join(", ")}`, scopes);
|
|
315
|
-
this.config = config;
|
|
316
345
|
this.scopes = scopes;
|
|
346
|
+
this.storage = storage ?? new DefaultTokenCacheStorage();
|
|
347
|
+
this.config = {
|
|
348
|
+
...config,
|
|
349
|
+
cache: {
|
|
350
|
+
...config.cache,
|
|
351
|
+
cachePlugin: buildDiskCoherencyCachePlugin(this.storage)
|
|
352
|
+
}
|
|
353
|
+
};
|
|
317
354
|
this.msalApp = new PublicClientApplication(this.config);
|
|
318
355
|
this.accessToken = null;
|
|
319
356
|
this.tokenExpiry = null;
|
|
@@ -323,7 +360,6 @@ class AuthManager {
|
|
|
323
360
|
this.expectedHomeAccountId = this.normalizeExpectedHomeAccountId(
|
|
324
361
|
expectedAccount?.expectedHomeAccountId
|
|
325
362
|
);
|
|
326
|
-
this.storage = storage ?? new DefaultTokenCacheStorage();
|
|
327
363
|
const oauthTokenFromEnv = process.env.MS365_MCP_OAUTH_TOKEN;
|
|
328
364
|
this.oauthToken = oauthTokenFromEnv ?? null;
|
|
329
365
|
this.isOAuthMode = oauthTokenFromEnv != null;
|
|
@@ -367,17 +403,6 @@ class AuthManager {
|
|
|
367
403
|
}
|
|
368
404
|
}
|
|
369
405
|
}
|
|
370
|
-
async saveTokenCache() {
|
|
371
|
-
try {
|
|
372
|
-
const stamped = wrapCache(this.msalApp.getTokenCache().serialize());
|
|
373
|
-
await this.storage.save("token-cache", stamped);
|
|
374
|
-
} catch (error) {
|
|
375
|
-
logger.error(`Error saving token cache: ${error.message}`);
|
|
376
|
-
if (this.storage.failClosed) {
|
|
377
|
-
throw error;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
406
|
async saveSelectedAccount() {
|
|
382
407
|
try {
|
|
383
408
|
const stamped = wrapCache(JSON.stringify({ accountId: this.selectedAccountId }));
|
|
@@ -491,7 +516,10 @@ class AuthManager {
|
|
|
491
516
|
try {
|
|
492
517
|
await this.msalApp.getTokenCache().removeAccount(account);
|
|
493
518
|
} catch (error) {
|
|
494
|
-
logger.
|
|
519
|
+
logger.error(`Failed to remove unexpected account from cache: ${error.message}`);
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Authenticated Microsoft account '${this.describeAccount(account)}' does not match expected Microsoft account '${this.expectedAccountLabel()}', and it could not be removed from the token cache (${error.message}). Its tokens may remain persisted - run --logout to clear the cache, then re-login.`
|
|
522
|
+
);
|
|
495
523
|
}
|
|
496
524
|
throw new Error(
|
|
497
525
|
`Authenticated Microsoft account '${this.describeAccount(account)}' does not match expected Microsoft account '${this.expectedAccountLabel()}'. Login was not persisted.`
|
|
@@ -522,7 +550,6 @@ class AuthManager {
|
|
|
522
550
|
const response = await this.msalApp.acquireTokenSilent(silentRequest);
|
|
523
551
|
this.accessToken = response.accessToken;
|
|
524
552
|
this.tokenExpiry = response.expiresOn ? new Date(response.expiresOn).getTime() : null;
|
|
525
|
-
await this.saveTokenCache();
|
|
526
553
|
return this.accessToken;
|
|
527
554
|
} catch (error) {
|
|
528
555
|
const hint = consumersAuthorityHint(error, currentAccount, this.config.auth.authority);
|
|
@@ -584,7 +611,6 @@ class AuthManager {
|
|
|
584
611
|
await this.saveSelectedAccount();
|
|
585
612
|
logger.info(`Auto-selected new account: ${response.account.username}`);
|
|
586
613
|
}
|
|
587
|
-
await this.saveTokenCache();
|
|
588
614
|
return this.accessToken;
|
|
589
615
|
} catch (error) {
|
|
590
616
|
logger.error(`Error in device code flow: ${error.message}`);
|
|
@@ -626,7 +652,6 @@ class AuthManager {
|
|
|
626
652
|
await this.saveSelectedAccount();
|
|
627
653
|
logger.info(`Auto-selected new account: ${response.account.username}`);
|
|
628
654
|
}
|
|
629
|
-
await this.saveTokenCache();
|
|
630
655
|
return this.accessToken;
|
|
631
656
|
} catch (error) {
|
|
632
657
|
logger.error(`Error in interactive browser flow: ${error.message}`);
|
|
@@ -842,7 +867,6 @@ class AuthManager {
|
|
|
842
867
|
};
|
|
843
868
|
try {
|
|
844
869
|
const response = await this.msalApp.acquireTokenSilent(silentRequest);
|
|
845
|
-
await this.saveTokenCache();
|
|
846
870
|
return response.accessToken;
|
|
847
871
|
} catch (error) {
|
|
848
872
|
const hint = consumersAuthorityHint(error, targetAccount, this.config.auth.authority);
|
|
@@ -858,6 +882,7 @@ class AuthManager {
|
|
|
858
882
|
var auth_default = AuthManager;
|
|
859
883
|
export {
|
|
860
884
|
buildAllowedScopeDiagnostics,
|
|
885
|
+
buildDiskCoherencyCachePlugin,
|
|
861
886
|
buildScopeDiagnostics,
|
|
862
887
|
buildScopesFromEndpoints,
|
|
863
888
|
collapseScopeHierarchy,
|
package/dist/cli.js
CHANGED
|
@@ -40,7 +40,7 @@ program.name("ms-365-mcp-server").description("Microsoft 365 MCP Server").versio
|
|
|
40
40
|
"Enable OAuth Dynamic Client Registration endpoint (kept for backwards compatibility, now enabled by default in HTTP mode)"
|
|
41
41
|
).option(
|
|
42
42
|
"--no-dynamic-registration",
|
|
43
|
-
"Disable OAuth Dynamic Client Registration endpoint in HTTP mode"
|
|
43
|
+
"Disable OAuth Dynamic Client Registration endpoint in HTTP mode. Equivalent env var: MS365_MCP_DISABLE_DCR=true."
|
|
44
44
|
).option(
|
|
45
45
|
"--auth-browser",
|
|
46
46
|
"Use browser-based interactive OAuth flow instead of device code for stdio mode. Opens system browser with localhost callback for seamless sign-in."
|
|
@@ -158,7 +158,8 @@ function parseArgs() {
|
|
|
158
158
|
options.toon = true;
|
|
159
159
|
}
|
|
160
160
|
if (options.http) {
|
|
161
|
-
|
|
161
|
+
const dcrDisabledByEnv = process.env.MS365_MCP_DISABLE_DCR === "true" || process.env.MS365_MCP_DISABLE_DCR === "1";
|
|
162
|
+
if (options.dynamicRegistration === false || dcrDisabledByEnv) {
|
|
162
163
|
options.enableDynamicRegistration = false;
|
|
163
164
|
} else {
|
|
164
165
|
options.enableDynamicRegistration = true;
|
package/dist/endpoints.json
CHANGED
|
@@ -1766,6 +1766,7 @@
|
|
|
1766
1766
|
"method": "post",
|
|
1767
1767
|
"toolName": "search-query",
|
|
1768
1768
|
"presets": ["personal", "search", "work"],
|
|
1769
|
+
"readOnly": true,
|
|
1769
1770
|
"workScopes": [
|
|
1770
1771
|
"Mail.Read",
|
|
1771
1772
|
"Calendars.Read",
|
|
@@ -1976,6 +1977,7 @@
|
|
|
1976
1977
|
"method": "post",
|
|
1977
1978
|
"toolName": "get-presences-by-user-id",
|
|
1978
1979
|
"presets": ["teams", "work"],
|
|
1980
|
+
"readOnly": true,
|
|
1979
1981
|
"workScopes": ["Presence.Read.All"],
|
|
1980
1982
|
"contentType": "application/json",
|
|
1981
1983
|
"llmTip": "Gets presence for multiple users in a single call. Body: { ids: ['user-id-1', 'user-id-2', ...] }. Returns array of presence objects. More efficient than calling get-user-presence for each user. Maximum 650 user IDs per request."
|
|
@@ -2259,6 +2261,7 @@
|
|
|
2259
2261
|
"method": "post",
|
|
2260
2262
|
"toolName": "get-mail-tips",
|
|
2261
2263
|
"presets": ["mail", "outlook", "personal"],
|
|
2264
|
+
"readOnly": true,
|
|
2262
2265
|
"scopes": ["Mail.Read"],
|
|
2263
2266
|
"contentType": "application/json",
|
|
2264
2267
|
"llmTip": "Looks up MailTips for one or more recipients before sending an email — answers 'is this person on auto-reply / OOF?', 'will my email exceed their mailbox quota?', 'are they an external recipient?', 'is this a mailbox or distribution list?'. Body: { EmailAddresses: ['user@contoso.com', ...], MailTipsOptions: 'automaticReplies, mailboxFullStatus, customMailTip, externalMemberCount, totalMemberCount, maxMessageSize, deliveryRestriction, moderationStatus, recipientScope, recipientSuggestions' (comma-separated subset) }. Returns mailTips per recipient with the requested fields populated. Use this to short-circuit urgent emails when a recipient is OOF, or to warn before fanning out to a large DL."
|
|
@@ -2429,6 +2432,7 @@
|
|
|
2429
2432
|
"method": "post",
|
|
2430
2433
|
"toolName": "get-onenote-notebook-from-web-url",
|
|
2431
2434
|
"presets": ["onenote", "personal"],
|
|
2435
|
+
"readOnly": true,
|
|
2432
2436
|
"workScopes": ["Notes.Read"],
|
|
2433
2437
|
"contentType": "application/json",
|
|
2434
2438
|
"llmTip": "Resolves a OneNote notebook from its web URL (the link a user copies from OneNote / SharePoint / Teams). Body: { webUrl: 'https://...' }. Returns a notebook object with id, displayName, sectionsUrl, sectionGroupsUrl, and isShared — you can then list its sections via list-onenote-notebook-sections. Accepts user notebooks, group notebooks, and SharePoint-hosted team notebooks. Personal Microsoft accounts are not supported."
|
package/dist/generated/client.js
CHANGED
|
@@ -291,7 +291,7 @@ const microsoft_graph_chatMessage = z.lazy(
|
|
|
291
291
|
deletedDateTime: z.string().regex(
|
|
292
292
|
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
293
293
|
).datetime({ offset: true }).describe(
|
|
294
|
-
"Read
|
|
294
|
+
"Read-only. Timestamp at which the chat message was deleted, or null if not deleted."
|
|
295
295
|
).nullish(),
|
|
296
296
|
etag: z.string().describe("Read-only. Version number of the chat message.").nullish(),
|
|
297
297
|
eventDetail: microsoft_graph_eventMessageDetail.optional(),
|
|
@@ -300,12 +300,12 @@ const microsoft_graph_chatMessage = z.lazy(
|
|
|
300
300
|
lastEditedDateTime: z.string().regex(
|
|
301
301
|
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
302
302
|
).datetime({ offset: true }).describe(
|
|
303
|
-
"Read
|
|
303
|
+
"Read-only. Timestamp when edits to the chat message were made. Triggers an 'Edited' flag in the Teams UI. If no edits are made the value is null."
|
|
304
304
|
).nullish(),
|
|
305
305
|
lastModifiedDateTime: z.string().regex(
|
|
306
306
|
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
307
307
|
).datetime({ offset: true }).describe(
|
|
308
|
-
"Read
|
|
308
|
+
"Read-only. Timestamp when the chat message is created (initial setting) or modified, including when a reaction is added or removed."
|
|
309
309
|
).nullish(),
|
|
310
310
|
locale: z.string().describe("Locale of the chat message set by the client. Always set to en-us.").optional(),
|
|
311
311
|
mentions: z.array(microsoft_graph_chatMessageMention).describe(
|
|
@@ -373,7 +373,7 @@ const microsoft_graph_targetedChatMessage = z.object({
|
|
|
373
373
|
lastModifiedDateTime: z.string().regex(
|
|
374
374
|
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
375
375
|
).datetime({ offset: true }).describe(
|
|
376
|
-
"Read
|
|
376
|
+
"Read-only. Timestamp when the chat message is created (initial setting) or modified, including when a reaction is added or removed."
|
|
377
377
|
).nullish(),
|
|
378
378
|
body: microsoft_graph_itemBody.optional(),
|
|
379
379
|
subject: z.string().describe("The subject of the chat message, in plaintext.").nullish(),
|
|
@@ -385,14 +385,14 @@ const microsoft_graph_targetedChatMessage = z.object({
|
|
|
385
385
|
deletedDateTime: z.string().regex(
|
|
386
386
|
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
387
387
|
).datetime({ offset: true }).describe(
|
|
388
|
-
"Read
|
|
388
|
+
"Read-only. Timestamp at which the chat message was deleted, or null if not deleted."
|
|
389
389
|
).nullish(),
|
|
390
390
|
etag: z.string().describe("Read-only. Version number of the chat message.").nullish(),
|
|
391
391
|
eventDetail: microsoft_graph_eventMessageDetail.optional(),
|
|
392
392
|
lastEditedDateTime: z.string().regex(
|
|
393
393
|
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
394
394
|
).datetime({ offset: true }).describe(
|
|
395
|
-
"Read
|
|
395
|
+
"Read-only. Timestamp when edits to the chat message were made. Triggers an 'Edited' flag in the Teams UI. If no edits are made the value is null."
|
|
396
396
|
).nullish(),
|
|
397
397
|
locale: z.string().describe("Locale of the chat message set by the client. Always set to en-us.").optional(),
|
|
398
398
|
mentions: z.array(microsoft_graph_chatMessageMention).describe(
|
|
@@ -675,7 +675,7 @@ const microsoft_graph_user = z.object({
|
|
|
675
675
|
"The state or province in the user's address. Maximum length is 128 characters. Requires $select to retrieve. Supports $filter (eq, ne, not, ge, le, in, startsWith, and eq on null values)."
|
|
676
676
|
).nullish(),
|
|
677
677
|
userPrincipalName: z.string().describe(
|
|
678
|
-
"The user principal name (UPN) of the user. The UPN is an Internet-style sign-in name for the user based on the Internet standard RFC 822. By convention, this value should map to the user's email name. The general format is alias@domain, where the domain must be present in the tenant's collection of verified domains. This property is required when a user is created. The verified domains for the tenant can be accessed from the verifiedDomains property of organization.NOTE: This property can't contain accent characters. Only the following characters are allowed A - Z, a - z, 0 - 9, ' . - _ ! # ^ ~. For the complete list of allowed characters, see username policies. Returned by default. Supports $filter (eq, ne, not, ge, le, in, startsWith, endsWith) and $orderby."
|
|
678
|
+
"The user principal name (UPN) of the user. The UPN is an Internet-style sign-in name for the user based on the Internet standard RFC 822. By convention, this value should map to the user's email name. The general format is alias@domain, where the domain must be present in the tenant's collection of verified domains. This property is required when a user is created. The verified domains for the tenant can be accessed from the verifiedDomains property of organization.NOTE: This property can't contain accent characters. Only the following characters are allowed A - Z, a - z, 0 - 9, ' . - _ ! # ^ ~. For the complete list of allowed characters, see username policies. Returned by default. Supports $filter (eq, ne, not, ge, le, in, startsWith, endsWith) and $orderby. This property is subject to sensitive action restrictions; only specific privileged administrator roles can update it."
|
|
679
679
|
).nullish(),
|
|
680
680
|
deletedDateTime: z.string().regex(
|
|
681
681
|
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
@@ -686,7 +686,7 @@ const microsoft_graph_user = z.object({
|
|
|
686
686
|
"A freeform text entry field for the user to describe themselves. Requires $select to retrieve."
|
|
687
687
|
).nullish(),
|
|
688
688
|
accountEnabled: z.boolean().describe(
|
|
689
|
-
"true if the account is enabled; otherwise, false. This property is required when a user is created. Requires $select to retrieve. Supports $filter (eq, ne, not, and in)."
|
|
689
|
+
"true if the account is enabled; otherwise, false. This property is required when a user is created. Requires $select to retrieve. Supports $filter (eq, ne, not, and in). This property is subject to sensitive action restrictions; only specific privileged administrator roles can update it."
|
|
690
690
|
).nullish(),
|
|
691
691
|
ageGroup: z.string().describe(
|
|
692
692
|
"Sets the age group of the user. Allowed values: null, Minor, NotAdult, and Adult. For more information, see legal age group property definitions. Requires $select to retrieve. Supports $filter (eq, ne, not, and in)."
|
|
@@ -704,7 +704,7 @@ const microsoft_graph_user = z.object({
|
|
|
704
704
|
"The birthday of the user. The Timestamp type represents date and time information using ISO 8601 format and is always in UTC. For example, midnight UTC on Jan 1, 2014, is 2014-01-01T00:00:00Z. Requires $select to retrieve."
|
|
705
705
|
).optional(),
|
|
706
706
|
businessPhones: z.array(z.string()).describe(
|
|
707
|
-
"The telephone numbers for the user. NOTE: Although it's a string collection, only one number can be set for this property. Read-only for users synced from the on-premises directory. Returned by default. Supports $filter (eq, not, ge, le, startsWith)."
|
|
707
|
+
"The telephone numbers for the user. NOTE: Although it's a string collection, only one number can be set for this property. Read-only for users synced from the on-premises directory. Returned by default. Supports $filter (eq, not, ge, le, startsWith). This property is subject to sensitive action restrictions; only specific privileged administrator roles can update it."
|
|
708
708
|
).optional(),
|
|
709
709
|
city: z.string().describe(
|
|
710
710
|
"The city where the user is located. Maximum length is 128 characters. Requires $select to retrieve. Supports $filter (eq, ne, not, ge, le, in, startsWith, and eq on null values)."
|
|
@@ -1578,6 +1578,13 @@ const microsoft_graph_workbookRange = z.object({
|
|
|
1578
1578
|
worksheet: microsoft_graph_workbookWorksheet.optional()
|
|
1579
1579
|
}).passthrough();
|
|
1580
1580
|
const create_excel_table_Body = z.object({ address: z.string().nullable(), hasHeaders: z.boolean().default(false) }).partial().passthrough();
|
|
1581
|
+
const microsoft_graph_groupAccessType = z.enum([
|
|
1582
|
+
"none",
|
|
1583
|
+
"private",
|
|
1584
|
+
"secret",
|
|
1585
|
+
"public",
|
|
1586
|
+
"unknownFutureValue"
|
|
1587
|
+
]);
|
|
1581
1588
|
const microsoft_graph_assignedLabel = z.object({
|
|
1582
1589
|
displayName: z.string().describe("The display name of the label. Read-only.").nullish(),
|
|
1583
1590
|
labelId: z.string().describe("The unique identifier of the label.").nullish()
|
|
@@ -1601,6 +1608,7 @@ const microsoft_graph_group = z.object({
|
|
|
1601
1608
|
).datetime({ offset: true }).describe(
|
|
1602
1609
|
"Date and time when this object was deleted. Always null when the object hasn't been deleted."
|
|
1603
1610
|
).nullish(),
|
|
1611
|
+
accessType: microsoft_graph_groupAccessType.optional(),
|
|
1604
1612
|
allowExternalSenders: z.boolean().describe(
|
|
1605
1613
|
"Indicates if people external to the organization can send messages to the group. The default value is false. Requires $select to retrieve. Supported only on the Get group API (GET /groups/{ID})."
|
|
1606
1614
|
).nullish(),
|
|
@@ -1640,6 +1648,9 @@ const microsoft_graph_group = z.object({
|
|
|
1640
1648
|
isAssignableToRole: z.boolean().describe(
|
|
1641
1649
|
"Indicates whether this group can be assigned to a Microsoft Entra role. Optional. This property can only be set while creating the group and is immutable. If set to true, the securityEnabled property must also be set to true, visibility must be Hidden, and the group can't be a dynamic group (that is, groupTypes can't contain DynamicMembership). Only callers with at least the Privileged Role Administrator role can set this property. The caller must also be assigned the RoleManagement.ReadWrite.Directory permission to set this property or update the membership of such groups. For more, see Using a group to manage Microsoft Entra role assignmentsUsing this feature requires a Microsoft Entra ID P1 license. Returned by default. Supports $filter (eq, ne, not)."
|
|
1642
1650
|
).nullish(),
|
|
1651
|
+
isFavorite: z.boolean().describe(
|
|
1652
|
+
"Indicates whether the user marked the group as favorite. Requires $select to retrieve. Supported only on the Get group API (GET /groups/{ID})."
|
|
1653
|
+
).nullish(),
|
|
1643
1654
|
isManagementRestricted: z.boolean().describe(
|
|
1644
1655
|
"Indicates whether the group is a member of a restricted management administrative unit. If not set, the default value is null and the default behavior is false. Read-only. To manage a group member of a restricted management administrative unit, the administrator or calling app must be assigned a Microsoft Entra role at the scope of the restricted management administrative unit. Requires $select to retrieve."
|
|
1645
1656
|
).nullish(),
|
|
@@ -1652,12 +1663,6 @@ const microsoft_graph_group = z.object({
|
|
|
1652
1663
|
).nullish(),
|
|
1653
1664
|
mailEnabled: z.boolean().describe(
|
|
1654
1665
|
"Specifies whether the group is mail-enabled. Required. Returned by default. Supports $filter (eq, ne, not)."
|
|
1655
|
-
).nullish(),
|
|
1656
|
-
mailNickname: z.string().describe(
|
|
1657
|
-
"The mail alias for the group, unique for Microsoft 365 groups in the organization. Maximum length is 64 characters. This property can contain only characters in the ASCII character set 0 - 127 except the following characters: @ () / [] ' ; : <> , SPACE. Required. Returned by default. Supports $filter (eq, ne, not, ge, le, in, startsWith, and eq on null values)."
|
|
1658
|
-
).nullish(),
|
|
1659
|
-
membershipRule: z.string().describe(
|
|
1660
|
-
"The rule that determines members for this group if the group is a dynamic group (groupTypes contains DynamicMembership). For more information about the syntax of the membership rule, see Membership Rules syntax. Returned by default. Supports $filter (eq, ne, not, ge, le, startsWith)."
|
|
1661
1666
|
).nullish()
|
|
1662
1667
|
}).passthrough().passthrough();
|
|
1663
1668
|
const microsoft_graph_groupCollectionResponse = z.object({
|
|
@@ -2895,7 +2900,7 @@ const microsoft_graph_channel = z.lazy(
|
|
|
2895
2900
|
id: z.string().describe("The unique identifier for an entity. Read-only.").optional(),
|
|
2896
2901
|
createdDateTime: z.string().regex(
|
|
2897
2902
|
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
2898
|
-
).datetime({ offset: true }).describe("Read
|
|
2903
|
+
).datetime({ offset: true }).describe("Read-only. Timestamp at which the channel was created.").nullish(),
|
|
2899
2904
|
description: z.string().describe("Optional textual description for the channel.").nullish(),
|
|
2900
2905
|
displayName: z.string().describe(
|
|
2901
2906
|
"Channel name as it will appear to the user in Microsoft Teams. The maximum length is 50 characters."
|
|
@@ -3007,7 +3012,7 @@ const microsoft_graph_team = z.lazy(
|
|
|
3007
3012
|
).nullish(),
|
|
3008
3013
|
allChannels: z.array(microsoft_graph_channel).describe("List of channels either hosted in or shared with the team (incoming channels).").optional(),
|
|
3009
3014
|
channels: z.array(microsoft_graph_channel).describe("The collection of channels and messages associated with the team.").optional(),
|
|
3010
|
-
group: microsoft_graph_group.describe("[Note: Simplified from
|
|
3015
|
+
group: microsoft_graph_group.describe("[Note: Simplified from 80 properties to 25 most common ones]").optional(),
|
|
3011
3016
|
incomingChannels: z.array(microsoft_graph_channel).describe("List of channels shared with the team.").optional(),
|
|
3012
3017
|
installedApps: z.array(microsoft_graph_teamsAppInstallation).describe("The apps installed in this team.").optional(),
|
|
3013
3018
|
members: z.array(microsoft_graph_conversationMember).describe("Members and owners of the team.").optional(),
|
|
@@ -4973,6 +4978,7 @@ const schemas = {
|
|
|
4973
4978
|
microsoft_graph_workbookRangeFormat,
|
|
4974
4979
|
microsoft_graph_workbookRange,
|
|
4975
4980
|
create_excel_table_Body,
|
|
4981
|
+
microsoft_graph_groupAccessType,
|
|
4976
4982
|
microsoft_graph_assignedLabel,
|
|
4977
4983
|
microsoft_graph_licenseProcessingState,
|
|
4978
4984
|
microsoft_graph_group,
|
|
@@ -6860,6 +6866,7 @@ You can search within a folder hierarchy, a whole drive, or files shared with th
|
|
|
6860
6866
|
).datetime({ offset: true }).describe(
|
|
6861
6867
|
"Date and time when this object was deleted. Always null when the object hasn't been deleted."
|
|
6862
6868
|
).nullish(),
|
|
6869
|
+
accessType: microsoft_graph_groupAccessType.optional(),
|
|
6863
6870
|
allowExternalSenders: z.boolean().describe(
|
|
6864
6871
|
"Indicates if people external to the organization can send messages to the group. The default value is false. Requires $select to retrieve. Supported only on the Get group API (GET /groups/{ID})."
|
|
6865
6872
|
).nullish(),
|
|
@@ -6899,6 +6906,9 @@ You can search within a folder hierarchy, a whole drive, or files shared with th
|
|
|
6899
6906
|
isAssignableToRole: z.boolean().describe(
|
|
6900
6907
|
"Indicates whether this group can be assigned to a Microsoft Entra role. Optional. This property can only be set while creating the group and is immutable. If set to true, the securityEnabled property must also be set to true, visibility must be Hidden, and the group can't be a dynamic group (that is, groupTypes can't contain DynamicMembership). Only callers with at least the Privileged Role Administrator role can set this property. The caller must also be assigned the RoleManagement.ReadWrite.Directory permission to set this property or update the membership of such groups. For more, see Using a group to manage Microsoft Entra role assignmentsUsing this feature requires a Microsoft Entra ID P1 license. Returned by default. Supports $filter (eq, ne, not)."
|
|
6901
6908
|
).nullish(),
|
|
6909
|
+
isFavorite: z.boolean().describe(
|
|
6910
|
+
"Indicates whether the user marked the group as favorite. Requires $select to retrieve. Supported only on the Get group API (GET /groups/{ID})."
|
|
6911
|
+
).nullish(),
|
|
6902
6912
|
isManagementRestricted: z.boolean().describe(
|
|
6903
6913
|
"Indicates whether the group is a member of a restricted management administrative unit. If not set, the default value is null and the default behavior is false. Read-only. To manage a group member of a restricted management administrative unit, the administrator or calling app must be assigned a Microsoft Entra role at the scope of the restricted management administrative unit. Requires $select to retrieve."
|
|
6904
6914
|
).nullish(),
|
|
@@ -6911,12 +6921,6 @@ You can search within a folder hierarchy, a whole drive, or files shared with th
|
|
|
6911
6921
|
).nullish(),
|
|
6912
6922
|
mailEnabled: z.boolean().describe(
|
|
6913
6923
|
"Specifies whether the group is mail-enabled. Required. Returned by default. Supports $filter (eq, ne, not)."
|
|
6914
|
-
).nullish(),
|
|
6915
|
-
mailNickname: z.string().describe(
|
|
6916
|
-
"The mail alias for the group, unique for Microsoft 365 groups in the organization. Maximum length is 64 characters. This property can contain only characters in the ASCII character set 0 - 127 except the following characters: @ () / [] ' ; : <> , SPACE. Required. Returned by default. Supports $filter (eq, ne, not, ge, le, in, startsWith, and eq on null values)."
|
|
6917
|
-
).nullish(),
|
|
6918
|
-
membershipRule: z.string().describe(
|
|
6919
|
-
"The rule that determines members for this group if the group is a dynamic group (groupTypes contains DynamicMembership). For more information about the syntax of the membership rule, see Membership Rules syntax. Returned by default. Supports $filter (eq, ne, not, ge, le, startsWith)."
|
|
6920
6924
|
).nullish()
|
|
6921
6925
|
}).passthrough().passthrough()
|
|
6922
6926
|
}
|
|
@@ -6973,6 +6977,7 @@ You can create or update the following types of group: By default, this operatio
|
|
|
6973
6977
|
).datetime({ offset: true }).describe(
|
|
6974
6978
|
"Date and time when this object was deleted. Always null when the object hasn't been deleted."
|
|
6975
6979
|
).nullish(),
|
|
6980
|
+
accessType: microsoft_graph_groupAccessType.optional(),
|
|
6976
6981
|
allowExternalSenders: z.boolean().describe(
|
|
6977
6982
|
"Indicates if people external to the organization can send messages to the group. The default value is false. Requires $select to retrieve. Supported only on the Get group API (GET /groups/{ID})."
|
|
6978
6983
|
).nullish(),
|
|
@@ -7012,6 +7017,9 @@ You can create or update the following types of group: By default, this operatio
|
|
|
7012
7017
|
isAssignableToRole: z.boolean().describe(
|
|
7013
7018
|
"Indicates whether this group can be assigned to a Microsoft Entra role. Optional. This property can only be set while creating the group and is immutable. If set to true, the securityEnabled property must also be set to true, visibility must be Hidden, and the group can't be a dynamic group (that is, groupTypes can't contain DynamicMembership). Only callers with at least the Privileged Role Administrator role can set this property. The caller must also be assigned the RoleManagement.ReadWrite.Directory permission to set this property or update the membership of such groups. For more, see Using a group to manage Microsoft Entra role assignmentsUsing this feature requires a Microsoft Entra ID P1 license. Returned by default. Supports $filter (eq, ne, not)."
|
|
7014
7019
|
).nullish(),
|
|
7020
|
+
isFavorite: z.boolean().describe(
|
|
7021
|
+
"Indicates whether the user marked the group as favorite. Requires $select to retrieve. Supported only on the Get group API (GET /groups/{ID})."
|
|
7022
|
+
).nullish(),
|
|
7015
7023
|
isManagementRestricted: z.boolean().describe(
|
|
7016
7024
|
"Indicates whether the group is a member of a restricted management administrative unit. If not set, the default value is null and the default behavior is false. Read-only. To manage a group member of a restricted management administrative unit, the administrator or calling app must be assigned a Microsoft Entra role at the scope of the restricted management administrative unit. Requires $select to retrieve."
|
|
7017
7025
|
).nullish(),
|
|
@@ -7024,12 +7032,6 @@ You can create or update the following types of group: By default, this operatio
|
|
|
7024
7032
|
).nullish(),
|
|
7025
7033
|
mailEnabled: z.boolean().describe(
|
|
7026
7034
|
"Specifies whether the group is mail-enabled. Required. Returned by default. Supports $filter (eq, ne, not)."
|
|
7027
|
-
).nullish(),
|
|
7028
|
-
mailNickname: z.string().describe(
|
|
7029
|
-
"The mail alias for the group, unique for Microsoft 365 groups in the organization. Maximum length is 64 characters. This property can contain only characters in the ASCII character set 0 - 127 except the following characters: @ () / [] ' ; : <> , SPACE. Required. Returned by default. Supports $filter (eq, ne, not, ge, le, in, startsWith, and eq on null values)."
|
|
7030
|
-
).nullish(),
|
|
7031
|
-
membershipRule: z.string().describe(
|
|
7032
|
-
"The rule that determines members for this group if the group is a dynamic group (groupTypes contains DynamicMembership). For more information about the syntax of the membership rule, see Membership Rules syntax. Returned by default. Supports $filter (eq, ne, not, ge, le, startsWith)."
|
|
7033
7035
|
).nullish()
|
|
7034
7036
|
}).passthrough().passthrough()
|
|
7035
7037
|
}
|
|
@@ -9320,7 +9322,7 @@ be returned for more than one recipient at one time. The requested MailTips are
|
|
|
9320
9322
|
method: "get",
|
|
9321
9323
|
path: "/me/inferenceClassification/overrides",
|
|
9322
9324
|
alias: "list-focused-inbox-overrides",
|
|
9323
|
-
description: `Get the overrides that a user has set up to always classify messages from certain senders in specific ways. Each override corresponds to an SMTP address of a sender. Initially, a user
|
|
9325
|
+
description: `Get the overrides that a user has set up to always classify messages from certain senders in specific ways. Each override corresponds to an SMTP address of a sender. Initially, a user doesn't have any overrides.`,
|
|
9324
9326
|
requestFormat: "json",
|
|
9325
9327
|
parameters: [
|
|
9326
9328
|
{
|
|
@@ -9555,7 +9557,7 @@ the new SMTP address is the only way to 'update' the override for this sender.`,
|
|
|
9555
9557
|
method: "get",
|
|
9556
9558
|
path: "/me/mailFolders",
|
|
9557
9559
|
alias: "list-mail-folders",
|
|
9558
|
-
description: `Get the mail folder collection directly under the root folder of the signed-in user. The returned collection includes any mail search folders directly under the root. By default, this operation
|
|
9560
|
+
description: `Get the mail folder collection directly under the root folder of the signed-in user. The returned collection includes any mail search folders directly under the root. By default, this operation doesn't return hidden folders. Use a query parameter includeHiddenFolders to include them in the response. This operation doesn't return all mail folders in a mailbox, only the child folders of the root folder. To return all mail folders in a mailbox, each child folder must be traversed separately.`,
|
|
9559
9561
|
requestFormat: "json",
|
|
9560
9562
|
parameters: [
|
|
9561
9563
|
{
|
|
@@ -9658,7 +9660,7 @@ the new SMTP address is the only way to 'update' the override for this sender.`,
|
|
|
9658
9660
|
path: "/me/mailFolders/:mailFolderId/childFolders",
|
|
9659
9661
|
alias: "list-mail-child-folders",
|
|
9660
9662
|
description: `Get the folder collection under the specified folder. You can use the .../me/mailFolders shortcut to get the top-level
|
|
9661
|
-
folder collection and navigate to another folder. By default, this operation
|
|
9663
|
+
folder collection and navigate to another folder. By default, this operation doesn't return hidden folders. Use a query parameter includeHiddenFolders to include them in the response.`,
|
|
9662
9664
|
requestFormat: "json",
|
|
9663
9665
|
parameters: [
|
|
9664
9666
|
{
|
package/dist/graph-tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "crypto";
|
|
2
2
|
import logger from "./logger.js";
|
|
3
3
|
import { auditLog, getUserIdentityForAudit } from "./audit-log.js";
|
|
4
|
+
import { isDestructiveOperation } from "./lib/destructive-ops.js";
|
|
4
5
|
import {
|
|
5
6
|
getEndpointScopeGroups,
|
|
6
7
|
getMissingAllowedScopesForGroups,
|
|
@@ -67,6 +68,9 @@ function paginationAllowed() {
|
|
|
67
68
|
if (raw === void 0 || raw === "") return true;
|
|
68
69
|
return !/^(0|false|no)$/i.test(raw.trim());
|
|
69
70
|
}
|
|
71
|
+
function isConfirmGateEnabled() {
|
|
72
|
+
return process.env.MS365_MCP_REQUIRE_CONFIRM === "true";
|
|
73
|
+
}
|
|
70
74
|
function formatDisabledToolsForLog(disabledTools) {
|
|
71
75
|
const shown = disabledTools.slice(0, 20).map((tool) => `${tool.toolName} (missing: ${tool.missingScopes.join(", ")})`);
|
|
72
76
|
const suffix = disabledTools.length > shown.length ? `, ... +${disabledTools.length - shown.length} more` : "";
|
|
@@ -390,6 +394,26 @@ function registerUtilityToolWithMcp(server, utility, ctx) {
|
|
|
390
394
|
}
|
|
391
395
|
async function executeGraphTool(tool, config, graphClient, params, authManager) {
|
|
392
396
|
logger.info(`Tool ${tool.alias} called with params: ${JSON.stringify(params)}`);
|
|
397
|
+
if (isConfirmGateEnabled() && isDestructiveOperation(tool.method, config) && params.confirm !== true) {
|
|
398
|
+
logger.warn(
|
|
399
|
+
`Refusing destructive tool ${tool.alias} (${tool.method.toUpperCase()}): missing confirm: true`
|
|
400
|
+
);
|
|
401
|
+
return {
|
|
402
|
+
content: [
|
|
403
|
+
{
|
|
404
|
+
type: "text",
|
|
405
|
+
text: JSON.stringify({
|
|
406
|
+
error: "confirmation_required",
|
|
407
|
+
tool: tool.alias,
|
|
408
|
+
method: tool.method.toUpperCase(),
|
|
409
|
+
destructive: true,
|
|
410
|
+
message: 'This tool modifies user data. Re-call with parameter "confirm": true after the user has explicitly approved the operation.'
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
],
|
|
414
|
+
isError: true
|
|
415
|
+
};
|
|
416
|
+
}
|
|
393
417
|
const requestId = randomUUID();
|
|
394
418
|
const startTime = Date.now();
|
|
395
419
|
const upn = getUserIdentityForAudit(getRequestTokens()?.accessToken);
|
|
@@ -427,6 +451,7 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
427
451
|
for (const [paramName, paramValue] of Object.entries(params)) {
|
|
428
452
|
if ([
|
|
429
453
|
"account",
|
|
454
|
+
"confirm",
|
|
430
455
|
"fetchAllPages",
|
|
431
456
|
"includeHeaders",
|
|
432
457
|
"excludeResponse",
|
|
@@ -815,6 +840,12 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
815
840
|
}
|
|
816
841
|
paramSchema["includeHeaders"] = z.boolean().describe("Include response headers (including ETag) in the response metadata").optional();
|
|
817
842
|
paramSchema["excludeResponse"] = z.boolean().describe("Exclude the full response body and only return success or failure indication").optional();
|
|
843
|
+
const destructive = isDestructiveOperation(tool.method, endpointConfig);
|
|
844
|
+
if (destructive) {
|
|
845
|
+
paramSchema["confirm"] = z.boolean().describe(
|
|
846
|
+
'For destructive operations when the confirm gate is enabled (MS365_MCP_REQUIRE_CONFIRM=true; off by default). Set to true only after the user has explicitly approved this action. When the gate is on, calls without confirm: true return { error: "confirmation_required" } without touching user data.'
|
|
847
|
+
).optional();
|
|
848
|
+
}
|
|
818
849
|
if (endpointConfig?.supportsTimezone) {
|
|
819
850
|
paramSchema["timezone"] = z.string().describe(
|
|
820
851
|
'IANA timezone name (e.g., "America/New_York", "Europe/London", "Asia/Tokyo") for calendar event times. If not specified, times are returned in UTC.'
|
|
@@ -843,7 +874,7 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
843
874
|
{
|
|
844
875
|
title: tool.alias,
|
|
845
876
|
readOnlyHint: isReadOnlyTool,
|
|
846
|
-
destructiveHint:
|
|
877
|
+
destructiveHint: destructive,
|
|
847
878
|
openWorldHint: true
|
|
848
879
|
// All tools call Microsoft Graph API
|
|
849
880
|
},
|
|
@@ -1117,11 +1148,7 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
|
|
|
1117
1148
|
async ({ tool_name }) => {
|
|
1118
1149
|
const entry = toolsRegistry.get(tool_name);
|
|
1119
1150
|
if (entry) {
|
|
1120
|
-
const schema = describeToolSchema(
|
|
1121
|
-
entry.tool,
|
|
1122
|
-
entry.config?.llmTip,
|
|
1123
|
-
entry.config?.descriptionOverride
|
|
1124
|
-
);
|
|
1151
|
+
const schema = describeToolSchema(entry.tool, entry.config);
|
|
1125
1152
|
return {
|
|
1126
1153
|
content: [{ type: "text", text: JSON.stringify(schema, null, 2) }]
|
|
1127
1154
|
};
|
|
@@ -1196,6 +1223,7 @@ export {
|
|
|
1196
1223
|
UTILITY_TOOLS,
|
|
1197
1224
|
buildDiscoverySearchIndex,
|
|
1198
1225
|
buildToolsRegistry,
|
|
1226
|
+
isDestructiveOperation,
|
|
1199
1227
|
registerDiscoveryTools,
|
|
1200
1228
|
registerGraphTools,
|
|
1201
1229
|
scoreDiscoveryQuery
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
function isDestructiveOperation(method, config) {
|
|
2
|
+
const upper = method.toUpperCase();
|
|
3
|
+
if (!["POST", "PATCH", "PUT", "DELETE"].includes(upper)) return false;
|
|
4
|
+
if (upper === "POST" && config?.readOnly) return false;
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
isDestructiveOperation
|
|
9
|
+
};
|
package/dist/lib/tool-schema.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
2
|
+
import { isDestructiveOperation } from "./destructive-ops.js";
|
|
2
3
|
function unwrapOptional(schema) {
|
|
3
4
|
const def = schema._def;
|
|
4
5
|
const typeName = def?.typeName;
|
|
@@ -7,7 +8,7 @@ function unwrapOptional(schema) {
|
|
|
7
8
|
}
|
|
8
9
|
return { inner: schema, optional: false };
|
|
9
10
|
}
|
|
10
|
-
function describeToolSchema(tool,
|
|
11
|
+
function describeToolSchema(tool, config) {
|
|
11
12
|
const params = (tool.parameters ?? []).map((p) => {
|
|
12
13
|
const { inner, optional } = unwrapOptional(p.schema);
|
|
13
14
|
const isPath = p.type === "Path";
|
|
@@ -21,11 +22,21 @@ function describeToolSchema(tool, llmTip, descriptionOverride) {
|
|
|
21
22
|
schema
|
|
22
23
|
};
|
|
23
24
|
});
|
|
25
|
+
if (isDestructiveOperation(tool.method, config)) {
|
|
26
|
+
params.push({
|
|
27
|
+
name: "confirm",
|
|
28
|
+
in: "Query",
|
|
29
|
+
required: false,
|
|
30
|
+
description: 'For destructive operations when the confirm gate is enabled (MS365_MCP_REQUIRE_CONFIRM=true; off by default). Set to true only after the user has explicitly approved this action. When the gate is on, calls without confirm: true return { error: "confirmation_required" } without touching user data.',
|
|
31
|
+
schema: { type: "boolean" }
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const llmTip = config?.llmTip;
|
|
24
35
|
return {
|
|
25
36
|
name: tool.alias,
|
|
26
37
|
method: tool.method.toUpperCase(),
|
|
27
38
|
path: tool.path,
|
|
28
|
-
description: descriptionOverride ?? tool.description ?? "",
|
|
39
|
+
description: config?.descriptionOverride ?? tool.description ?? "",
|
|
29
40
|
...llmTip ? { llmTip } : {},
|
|
30
41
|
parameters: params
|
|
31
42
|
};
|
package/docs/deployment.md
CHANGED
|
@@ -212,8 +212,10 @@ The client automatically discovers OAuth endpoints and opens a browser for authe
|
|
|
212
212
|
- **Read-only mode**: use `--read-only` to disable all write operations (send, delete, update, create)
|
|
213
213
|
- **Tool filtering**: use `--enabled-tools <regex>` or `--preset <names>` to restrict available tools
|
|
214
214
|
- **CORS**: configure `MS365_MCP_CORS_ORIGIN` to restrict allowed origins (defaults to `http://localhost:3000`); set explicitly when clients run on a different origin
|
|
215
|
+
- **Disable Dynamic Client Registration**: when only a known client talks to the server, set `MS365_MCP_DISABLE_DCR=true` (or pass `--no-dynamic-registration`) to close the anonymous `/register` endpoint
|
|
215
216
|
- **Structured audit log**: enabled by default. Every tool invocation emits one JSON line on stdout (captured by the container platform's log collector) and to `~/.ms-365-mcp-server/logs/audit.log` (mode `0o600`) with `{ event, request_id, user_principal_name, tool, http_method, status, duration_ms, error_type?, error_code? }`. The schema is intentionally narrow — tool parameters and Graph response bodies are NEVER recorded, and error messages are reduced to `error_type` / `error_code` so upstream library errors do not leak token fragments or query-string PII. Forms the "who accessed what, when" trail required for GDPR / HIPAA / PIPEDA / SOC 2 audit. Opt-out: `MS365_MCP_AUDIT_LOG=false`
|
|
216
217
|
- **Graph resilience**: every call to Microsoft Graph is wrapped with a fetch timeout (default 100 s via `MS365_MCP_GRAPH_TIMEOUT_MS`), retry-with-backoff on 429 / 503 / 504 / network errors (default 3 retries, full-jitter exponential backoff, honours `Retry-After`; 503 / 504 / network errors only retried for idempotent methods, 429 retried on all methods), and a process-wide circuit breaker that opens after 5 consecutive failures and cools down for 30 s (`MS365_MCP_GRAPH_CIRCUIT_THRESHOLD` / `MS365_MCP_GRAPH_CIRCUIT_COOLDOWN_MS`). Disable the breaker for trusted automation: `MS365_MCP_GRAPH_CIRCUIT_DISABLED=true`
|
|
218
|
+
- **Confirm gate on destructive tools**: opt-in, **off by default**. Enable with `MS365_MCP_REQUIRE_CONFIRM=true`. When on, destructive tools (POST except `readOnly`, PATCH, PUT, DELETE — `delete-mail-message`, `send-mail`, `update-event`, etc.) return `{ "error": "confirmation_required" }` until the caller re-invokes them with `"confirm": true`. Mitigates accidental writes when an LLM misroutes a request or follows an injected instruction. Shipped opt-in so it is a non-breaking, additive layer that can coexist with client-side elicitation prompts (MCP Elicitation API) where the client supports them.
|
|
217
219
|
|
|
218
220
|
## Exposed Endpoints
|
|
219
221
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.129.0",
|
|
4
4
|
"description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/src/endpoints.json
CHANGED
|
@@ -1766,6 +1766,7 @@
|
|
|
1766
1766
|
"method": "post",
|
|
1767
1767
|
"toolName": "search-query",
|
|
1768
1768
|
"presets": ["personal", "search", "work"],
|
|
1769
|
+
"readOnly": true,
|
|
1769
1770
|
"workScopes": [
|
|
1770
1771
|
"Mail.Read",
|
|
1771
1772
|
"Calendars.Read",
|
|
@@ -1976,6 +1977,7 @@
|
|
|
1976
1977
|
"method": "post",
|
|
1977
1978
|
"toolName": "get-presences-by-user-id",
|
|
1978
1979
|
"presets": ["teams", "work"],
|
|
1980
|
+
"readOnly": true,
|
|
1979
1981
|
"workScopes": ["Presence.Read.All"],
|
|
1980
1982
|
"contentType": "application/json",
|
|
1981
1983
|
"llmTip": "Gets presence for multiple users in a single call. Body: { ids: ['user-id-1', 'user-id-2', ...] }. Returns array of presence objects. More efficient than calling get-user-presence for each user. Maximum 650 user IDs per request."
|
|
@@ -2259,6 +2261,7 @@
|
|
|
2259
2261
|
"method": "post",
|
|
2260
2262
|
"toolName": "get-mail-tips",
|
|
2261
2263
|
"presets": ["mail", "outlook", "personal"],
|
|
2264
|
+
"readOnly": true,
|
|
2262
2265
|
"scopes": ["Mail.Read"],
|
|
2263
2266
|
"contentType": "application/json",
|
|
2264
2267
|
"llmTip": "Looks up MailTips for one or more recipients before sending an email — answers 'is this person on auto-reply / OOF?', 'will my email exceed their mailbox quota?', 'are they an external recipient?', 'is this a mailbox or distribution list?'. Body: { EmailAddresses: ['user@contoso.com', ...], MailTipsOptions: 'automaticReplies, mailboxFullStatus, customMailTip, externalMemberCount, totalMemberCount, maxMessageSize, deliveryRestriction, moderationStatus, recipientScope, recipientSuggestions' (comma-separated subset) }. Returns mailTips per recipient with the requested fields populated. Use this to short-circuit urgent emails when a recipient is OOF, or to warn before fanning out to a large DL."
|
|
@@ -2429,6 +2432,7 @@
|
|
|
2429
2432
|
"method": "post",
|
|
2430
2433
|
"toolName": "get-onenote-notebook-from-web-url",
|
|
2431
2434
|
"presets": ["onenote", "personal"],
|
|
2435
|
+
"readOnly": true,
|
|
2432
2436
|
"workScopes": ["Notes.Read"],
|
|
2433
2437
|
"contentType": "application/json",
|
|
2434
2438
|
"llmTip": "Resolves a OneNote notebook from its web URL (the link a user copies from OneNote / SharePoint / Teams). Body: { webUrl: 'https://...' }. Returns a notebook object with id, displayName, sectionsUrl, sectionGroupsUrl, and isShared — you can then list its sections via list-onenote-notebook-sections. Accepts user notebooks, group notebooks, and SharePoint-hosted team notebooks. Personal Microsoft accounts are not supported."
|