@softeria/ms-365-mcp-server 0.157.2 → 0.158.1

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.
@@ -1,8 +1,8 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { z } from "zod";
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
3
2
  import { tmpdir } from "os";
4
3
  import { join } from "path";
5
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { z } from "zod";
6
6
  const loggerMock = vi.hoisted(() => ({
7
7
  info: vi.fn(),
8
8
  warn: vi.fn(),
@@ -2331,16 +2331,11 @@ describe("graph-tools", () => {
2331
2331
  };
2332
2332
  const server = createMockServer();
2333
2333
  const { registerGraphTools } = await loadModule();
2334
- registerGraphTools(
2335
- server,
2336
- graphClient,
2337
- false,
2338
- void 0,
2339
- false,
2334
+ registerGraphTools(server, graphClient, {
2340
2335
  authManager,
2341
- true,
2342
- ["user1@domain.com", "user2@domain.com"]
2343
- );
2336
+ multiAccount: true,
2337
+ accountNames: ["user1@domain.com", "user2@domain.com"]
2338
+ });
2344
2339
  const outputPath = join(tmpDir, "invoice.pdf");
2345
2340
  const result = await server.tools.get("download-bytes-to-file").handler({
2346
2341
  target: "/me/messages/m1/attachments/a1/$value",
@@ -2364,18 +2359,7 @@ describe("graph-tools", () => {
2364
2359
  expect(stdioServer.tools.has("download-bytes-to-file")).toBe(true);
2365
2360
  expect(stdioServer.tools.has("download-bytes")).toBe(true);
2366
2361
  const httpServer = createMockServer();
2367
- registerGraphTools(
2368
- httpServer,
2369
- {},
2370
- false,
2371
- void 0,
2372
- false,
2373
- void 0,
2374
- false,
2375
- [],
2376
- void 0,
2377
- true
2378
- );
2362
+ registerGraphTools(httpServer, {}, { httpMode: true });
2379
2363
  expect(httpServer.tools.has("download-bytes-to-file")).toBe(false);
2380
2364
  expect(httpServer.tools.has("download-bytes")).toBe(true);
2381
2365
  });
@@ -2730,16 +2714,11 @@ describe("graph-tools", () => {
2730
2714
  };
2731
2715
  const server = createMockServer();
2732
2716
  const { registerGraphTools } = await loadModule();
2733
- registerGraphTools(
2734
- server,
2735
- graphClient,
2736
- false,
2737
- void 0,
2738
- false,
2717
+ registerGraphTools(server, graphClient, {
2739
2718
  authManager,
2740
- true,
2741
- ["user1@domain.com", "user2@domain.com"]
2742
- );
2719
+ multiAccount: true,
2720
+ accountNames: ["user1@domain.com", "user2@domain.com"]
2721
+ });
2743
2722
  const { requestContext } = await import("../request-context.js");
2744
2723
  const tool = server.tools.get("get-download-url");
2745
2724
  const bearer = makeJwt({ upn: "user1@domain.com" });
@@ -2790,17 +2769,9 @@ describe("graph-tools", () => {
2790
2769
  ];
2791
2770
  const server = createMockServer();
2792
2771
  const { registerGraphTools } = await loadModule();
2793
- registerGraphTools(
2794
- server,
2795
- createMockGraphClient(),
2796
- false,
2797
- void 0,
2798
- false,
2799
- void 0,
2800
- false,
2801
- [],
2802
- "Mail.Read"
2803
- );
2772
+ registerGraphTools(server, createMockGraphClient(), {
2773
+ allowedScopes: "Mail.Read"
2774
+ });
2804
2775
  expect(server.tools.has("list-mail-messages")).toBe(true);
2805
2776
  expect(server.tools.has("list-calendar-events")).toBe(false);
2806
2777
  });
@@ -2825,17 +2796,9 @@ describe("graph-tools", () => {
2825
2796
  ];
2826
2797
  const server = createMockServer();
2827
2798
  const { registerGraphTools } = await loadModule();
2828
- registerGraphTools(
2829
- server,
2830
- createMockGraphClient(),
2831
- false,
2832
- void 0,
2833
- false,
2834
- void 0,
2835
- false,
2836
- [],
2837
- "Mail.Read"
2838
- );
2799
+ registerGraphTools(server, createMockGraphClient(), {
2800
+ allowedScopes: "Mail.Read"
2801
+ });
2839
2802
  const handler = server.server._requestHandlers.get("tools/call");
2840
2803
  await expect(
2841
2804
  handler?.(
@@ -2896,17 +2859,7 @@ describe("graph-tools", () => {
2896
2859
  ];
2897
2860
  const server = createMockServer();
2898
2861
  const { registerDiscoveryTools } = await loadModule();
2899
- registerDiscoveryTools(
2900
- server,
2901
- {},
2902
- false,
2903
- false,
2904
- void 0,
2905
- false,
2906
- [],
2907
- void 0,
2908
- "Mail.Read"
2909
- );
2862
+ registerDiscoveryTools(server, {}, { allowedScopes: "Mail.Read" });
2910
2863
  const result = await server.tools.get("search-tools").handler({ limit: 50 });
2911
2864
  const found = JSON.parse(result.content[0].text).tools.map((t) => t.name);
2912
2865
  expect(found).toContain("list-mail-messages");
@@ -2933,17 +2886,7 @@ describe("graph-tools", () => {
2933
2886
  ];
2934
2887
  const server = createMockServer();
2935
2888
  const { registerDiscoveryTools } = await loadModule();
2936
- registerDiscoveryTools(
2937
- server,
2938
- {},
2939
- false,
2940
- false,
2941
- void 0,
2942
- false,
2943
- [],
2944
- void 0,
2945
- "Mail.Read"
2946
- );
2889
+ registerDiscoveryTools(server, {}, { allowedScopes: "Mail.Read" });
2947
2890
  const result = await server.tools.get("execute-tool").handler({
2948
2891
  tool_name: "get-drive-item",
2949
2892
  parameters: { driveId: "drive-1", driveItemId: "item-2" }
@@ -2985,17 +2928,7 @@ describe("graph-tools", () => {
2985
2928
  ];
2986
2929
  const server = createMockServer();
2987
2930
  const { registerDiscoveryTools } = await loadModule();
2988
- registerDiscoveryTools(
2989
- server,
2990
- {},
2991
- false,
2992
- false,
2993
- void 0,
2994
- false,
2995
- [],
2996
- void 0,
2997
- "Mail.Read"
2998
- );
2931
+ registerDiscoveryTools(server, {}, { allowedScopes: "Mail.Read" });
2999
2932
  const handler = server.server._requestHandlers.get("tools/call");
3000
2933
  await expect(
3001
2934
  handler?.(
@@ -3121,7 +3054,7 @@ describe("graph-tools", () => {
3121
3054
  ];
3122
3055
  const server = createMockServer();
3123
3056
  const { registerDiscoveryTools } = await loadModule();
3124
- registerDiscoveryTools(server, {}, false, false, void 0, false, [], "mail");
3057
+ registerDiscoveryTools(server, {}, { enabledTools: "mail" });
3125
3058
  const result = await server.tools.get("search-tools").handler({ limit: 50 });
3126
3059
  const found = JSON.parse(result.content[0].text).tools.map((t) => t.name);
3127
3060
  expect(found).toContain("list-mail-messages");
@@ -3157,16 +3090,7 @@ describe("graph-tools", () => {
3157
3090
  ];
3158
3091
  const server = createMockServer();
3159
3092
  const { registerDiscoveryTools } = await loadModule();
3160
- registerDiscoveryTools(
3161
- server,
3162
- {},
3163
- false,
3164
- false,
3165
- void 0,
3166
- false,
3167
- [],
3168
- "^list-mail-messages$"
3169
- );
3093
+ registerDiscoveryTools(server, {}, { enabledTools: "^list-mail-messages$" });
3170
3094
  const result = await server.tools.get("execute-tool").handler({
3171
3095
  tool_name: "get-drive-item",
3172
3096
  parameters: { driveId: "drive-1", driveItemId: "item-2" }
@@ -3191,16 +3115,7 @@ describe("graph-tools", () => {
3191
3115
  mockEndpointsJson = [];
3192
3116
  const server = createMockServer();
3193
3117
  const { registerDiscoveryTools } = await loadModule();
3194
- registerDiscoveryTools(
3195
- server,
3196
- {},
3197
- false,
3198
- false,
3199
- void 0,
3200
- false,
3201
- [],
3202
- "^download-bytes$"
3203
- );
3118
+ registerDiscoveryTools(server, {}, { enabledTools: "^download-bytes$" });
3204
3119
  const result = await server.tools.get("search-tools").handler({ limit: 50 });
3205
3120
  const found = JSON.parse(result.content[0].text).tools.map((t) => t.name);
3206
3121
  expect(found).toContain("download-bytes");
@@ -3211,16 +3126,7 @@ describe("graph-tools", () => {
3211
3126
  mockEndpointsJson = [];
3212
3127
  const server = createMockServer();
3213
3128
  const { registerDiscoveryTools } = await loadModule();
3214
- registerDiscoveryTools(
3215
- server,
3216
- {},
3217
- false,
3218
- false,
3219
- void 0,
3220
- false,
3221
- [],
3222
- "[invalid"
3223
- );
3129
+ registerDiscoveryTools(server, {}, { enabledTools: "[invalid" });
3224
3130
  const result = await server.tools.get("search-tools").handler({ limit: 50 });
3225
3131
  const found = JSON.parse(result.content[0].text).tools.map((t) => t.name);
3226
3132
  expect(found).toContain("download-bytes");
@@ -3233,7 +3139,7 @@ describe("graph-tools", () => {
3233
3139
  mockEndpointsJson = [];
3234
3140
  const server = createMockServer();
3235
3141
  const { registerGraphTools } = await loadModule();
3236
- registerGraphTools(server, {}, true);
3142
+ registerGraphTools(server, {}, { readOnly: true });
3237
3143
  expect(server.tools.has("download-bytes")).toBe(true);
3238
3144
  expect(server.tools.has("parse-teams-url")).toBe(true);
3239
3145
  });
@@ -3485,6 +3391,387 @@ describe("graph-tools", () => {
3485
3391
  joinWebUrl: "https://teams/x"
3486
3392
  });
3487
3393
  });
3394
+ it("restricts list-users selects and responses to the configured user fields", async () => {
3395
+ mockEndpoints.push(makeEndpoint({ alias: "list-users", path: "/users" }));
3396
+ mockEndpointsJson = [
3397
+ makeConfig({ toolName: "list-users", pathPattern: "/users", scopes: ["User.Read.All"] })
3398
+ ];
3399
+ const graphClient = createMockGraphClient([
3400
+ {
3401
+ content: [
3402
+ {
3403
+ type: "text",
3404
+ text: JSON.stringify({
3405
+ value: [
3406
+ { id: "1", displayName: "Carlos", mail: "carlos@example.com", jobTitle: "CEO" }
3407
+ ]
3408
+ })
3409
+ }
3410
+ ]
3411
+ }
3412
+ ]);
3413
+ const server = createMockServer();
3414
+ const { registerGraphTools } = await loadModule();
3415
+ registerGraphTools(server, graphClient, {
3416
+ userFields: "id,displayName,mail"
3417
+ });
3418
+ const result = await server.tools.get("list-users").handler({
3419
+ select: "id,displayName,mail,jobTitle"
3420
+ });
3421
+ expect(graphClient.graphRequest.mock.calls[0][0]).toContain("$select=id,displayName,mail");
3422
+ expect(graphClient.graphRequest.mock.calls[0][0]).not.toContain("jobTitle");
3423
+ expect(JSON.parse(result.content[0].text).value[0]).toEqual({
3424
+ id: "1",
3425
+ displayName: "Carlos",
3426
+ mail: "carlos@example.com"
3427
+ });
3428
+ });
3429
+ it("adds the configured fields when list-users is called without select", async () => {
3430
+ mockEndpoints.push(makeEndpoint({ alias: "list-users", path: "/users" }));
3431
+ mockEndpointsJson = [
3432
+ makeConfig({ toolName: "list-users", pathPattern: "/users", scopes: ["User.Read.All"] })
3433
+ ];
3434
+ const graphClient = createMockGraphClient([
3435
+ {
3436
+ content: [
3437
+ {
3438
+ type: "text",
3439
+ text: JSON.stringify({
3440
+ value: [{ id: "1", displayName: "Carlos", jobTitle: "CEO" }]
3441
+ })
3442
+ }
3443
+ ]
3444
+ }
3445
+ ]);
3446
+ const server = createMockServer();
3447
+ const { registerGraphTools } = await loadModule();
3448
+ registerGraphTools(server, graphClient, { userFields: "id,displayName" });
3449
+ const result = await server.tools.get("list-users").handler({});
3450
+ expect(graphClient.graphRequest.mock.calls[0][0]).toContain("$select=id,displayName");
3451
+ expect(JSON.parse(result.content[0].text).value[0]).toEqual({
3452
+ id: "1",
3453
+ displayName: "Carlos"
3454
+ });
3455
+ });
3456
+ it("blocks expanded user data outside the list-users field boundary", async () => {
3457
+ mockEndpoints.push(makeEndpoint({ alias: "list-users", path: "/users" }));
3458
+ mockEndpointsJson = [
3459
+ makeConfig({ toolName: "list-users", pathPattern: "/users", scopes: ["User.Read.All"] })
3460
+ ];
3461
+ const graphClient = createMockGraphClient([
3462
+ {
3463
+ content: [
3464
+ {
3465
+ type: "text",
3466
+ text: JSON.stringify({
3467
+ value: [
3468
+ {
3469
+ id: "1",
3470
+ displayName: "Carlos",
3471
+ manager: { displayName: "Manager", jobTitle: "CEO" }
3472
+ }
3473
+ ]
3474
+ })
3475
+ }
3476
+ ]
3477
+ }
3478
+ ]);
3479
+ const server = createMockServer();
3480
+ const { registerGraphTools } = await loadModule();
3481
+ registerGraphTools(server, graphClient, { userFields: "id,displayName" });
3482
+ const result = await server.tools.get("list-users").handler({
3483
+ select: "id,displayName",
3484
+ expand: "manager($select=displayName,jobTitle)"
3485
+ });
3486
+ expect(graphClient.graphRequest.mock.calls[0][0]).not.toContain("$expand");
3487
+ expect(JSON.parse(result.content[0].text).value[0]).toEqual({
3488
+ id: "1",
3489
+ displayName: "Carlos"
3490
+ });
3491
+ });
3492
+ async function runListUsers(userFields, args, graphBody) {
3493
+ mockEndpoints.push(makeEndpoint({ alias: "list-users", path: "/users" }));
3494
+ mockEndpointsJson = [
3495
+ makeConfig({ toolName: "list-users", pathPattern: "/users", scopes: ["User.Read.All"] })
3496
+ ];
3497
+ const graphClient = createMockGraphClient([
3498
+ { content: [{ type: "text", text: JSON.stringify(graphBody) }] }
3499
+ ]);
3500
+ const server = createMockServer();
3501
+ const { registerGraphTools } = await loadModule();
3502
+ registerGraphTools(server, graphClient, { userFields });
3503
+ const result = await server.tools.get("list-users").handler(args);
3504
+ return { result, graphClient };
3505
+ }
3506
+ it("still projects when the requested fields are all disallowed", async () => {
3507
+ const { result, graphClient } = await runListUsers(
3508
+ "id,displayName",
3509
+ { select: "jobTitle,employeeId" },
3510
+ { value: [{ id: "1", displayName: "Carlos", jobTitle: "CEO" }] }
3511
+ );
3512
+ expect(graphClient.graphRequest.mock.calls[0][0]).toContain("$select=id,displayName");
3513
+ expect(JSON.parse(result.content[0].text).value[0]).toEqual({
3514
+ id: "1",
3515
+ displayName: "Carlos"
3516
+ });
3517
+ });
3518
+ it("drops id when the allowlist does not name it", async () => {
3519
+ const { result } = await runListUsers(
3520
+ "displayName",
3521
+ {},
3522
+ { value: [{ id: "1", displayName: "Carlos", jobTitle: "CEO" }] }
3523
+ );
3524
+ expect(JSON.parse(result.content[0].text).value[0]).toEqual({ displayName: "Carlos" });
3525
+ });
3526
+ it("keeps the pagination envelope while enforcing the boundary", async () => {
3527
+ const { result } = await runListUsers(
3528
+ "displayName",
3529
+ {},
3530
+ {
3531
+ "@odata.nextLink": "https://graph/next",
3532
+ value: [{ id: "1", displayName: "Carlos", jobTitle: "CEO" }]
3533
+ }
3534
+ );
3535
+ expect(JSON.parse(result.content[0].text)).toEqual({
3536
+ "@odata.nextLink": "https://graph/next",
3537
+ value: [{ displayName: "Carlos" }]
3538
+ });
3539
+ });
3540
+ it("fails closed when Graph returns none of the allowlisted fields", async () => {
3541
+ const { result } = await runListUsers(
3542
+ "displayName",
3543
+ {},
3544
+ { value: [{ id: "1", jobTitle: "CEO", employeeId: "E-1" }] }
3545
+ );
3546
+ expect(JSON.parse(result.content[0].text).value[0]).toEqual({});
3547
+ });
3548
+ it("refuses to register with an allowlist that names no fields", async () => {
3549
+ mockEndpoints.push(makeEndpoint({ alias: "list-users", path: "/users" }));
3550
+ mockEndpointsJson = [
3551
+ makeConfig({ toolName: "list-users", pathPattern: "/users", scopes: ["User.Read.All"] })
3552
+ ];
3553
+ const server = createMockServer();
3554
+ const { registerGraphTools } = await loadModule();
3555
+ expect(
3556
+ () => registerGraphTools(server, createMockGraphClient(), { userFields: " , ," })
3557
+ ).toThrow(/names no fields/);
3558
+ });
3559
+ it("enforces the boundary through discovery mode execute-tool", async () => {
3560
+ mockEndpoints.push(makeEndpoint({ alias: "list-users", path: "/users" }));
3561
+ mockEndpointsJson = [
3562
+ makeConfig({ toolName: "list-users", pathPattern: "/users", scopes: ["User.Read.All"] })
3563
+ ];
3564
+ const graphClient = createMockGraphClient([
3565
+ {
3566
+ content: [
3567
+ {
3568
+ type: "text",
3569
+ text: JSON.stringify({
3570
+ value: [{ id: "1", displayName: "Carlos", jobTitle: "CEO" }]
3571
+ })
3572
+ }
3573
+ ]
3574
+ }
3575
+ ]);
3576
+ const server = createMockServer();
3577
+ const { registerDiscoveryTools } = await loadModule();
3578
+ registerDiscoveryTools(server, graphClient, {
3579
+ userFields: "id,displayName"
3580
+ });
3581
+ const result = await server.tools.get("execute-tool").handler({
3582
+ tool_name: "list-users",
3583
+ parameters: { select: "id,displayName,jobTitle" }
3584
+ });
3585
+ expect(graphClient.graphRequest.mock.calls[0][0]).toContain("$select=id,displayName");
3586
+ expect(graphClient.graphRequest.mock.calls[0][0]).not.toContain("jobTitle");
3587
+ expect(JSON.parse(result.content[0].text).value[0]).toEqual({
3588
+ id: "1",
3589
+ displayName: "Carlos"
3590
+ });
3591
+ });
3592
+ it("projects a directory navigation that returns user profile data", async () => {
3593
+ mockEndpoints.push(
3594
+ makeEndpoint({ alias: "get-user-manager", path: "/users/:userId/manager" })
3595
+ );
3596
+ mockEndpointsJson = [
3597
+ makeConfig({
3598
+ toolName: "get-user-manager",
3599
+ pathPattern: "/users/{user-id}/manager",
3600
+ scopes: ["User.Read.All"]
3601
+ })
3602
+ ];
3603
+ const graphClient = createMockGraphClient([
3604
+ {
3605
+ content: [
3606
+ {
3607
+ type: "text",
3608
+ text: JSON.stringify({
3609
+ id: "m1",
3610
+ displayName: "Manager",
3611
+ mail: "manager@example.com",
3612
+ jobTitle: "CEO",
3613
+ employeeId: "E-9"
3614
+ })
3615
+ }
3616
+ ]
3617
+ }
3618
+ ]);
3619
+ const server = createMockServer();
3620
+ const { registerGraphTools } = await loadModule();
3621
+ registerGraphTools(server, graphClient, {
3622
+ userFields: "id,displayName,mail"
3623
+ });
3624
+ const result = await server.tools.get("get-user-manager").handler({ userId: "abc" });
3625
+ expect(graphClient.graphRequest.mock.calls[0][0]).not.toContain("$select");
3626
+ expect(JSON.parse(result.content[0].text)).toEqual({
3627
+ id: "m1",
3628
+ displayName: "Manager",
3629
+ mail: "manager@example.com"
3630
+ });
3631
+ });
3632
+ it("projects group members, which carry the same profile fields", async () => {
3633
+ mockEndpoints.push(
3634
+ makeEndpoint({ alias: "list-group-members", path: "/groups/:groupId/members" })
3635
+ );
3636
+ mockEndpointsJson = [
3637
+ makeConfig({
3638
+ toolName: "list-group-members",
3639
+ pathPattern: "/groups/{group-id}/members",
3640
+ scopes: ["GroupMember.Read.All"]
3641
+ })
3642
+ ];
3643
+ const graphClient = createMockGraphClient([
3644
+ {
3645
+ content: [
3646
+ {
3647
+ type: "text",
3648
+ text: JSON.stringify({
3649
+ value: [{ id: "1", displayName: "Carlos", jobTitle: "CEO" }]
3650
+ })
3651
+ }
3652
+ ]
3653
+ }
3654
+ ]);
3655
+ const server = createMockServer();
3656
+ const { registerGraphTools } = await loadModule();
3657
+ registerGraphTools(server, graphClient, { userFields: "id,displayName" });
3658
+ const result = await server.tools.get("list-group-members").handler({ groupId: "g1" });
3659
+ expect(JSON.parse(result.content[0].text).value[0]).toEqual({
3660
+ id: "1",
3661
+ displayName: "Carlos"
3662
+ });
3663
+ });
3664
+ it.each([
3665
+ { alias: "get-current-user", path: "/me", pathPattern: "/me", args: {} },
3666
+ {
3667
+ alias: "get-group",
3668
+ path: "/groups/:groupId",
3669
+ pathPattern: "/groups/{group-id}",
3670
+ args: { groupId: "g1" }
3671
+ }
3672
+ ])("strips $expand from $alias while the field policy is active", async (endpoint) => {
3673
+ mockEndpoints.push(makeEndpoint({ alias: endpoint.alias, path: endpoint.path }));
3674
+ mockEndpointsJson = [
3675
+ makeConfig({
3676
+ toolName: endpoint.alias,
3677
+ pathPattern: endpoint.pathPattern,
3678
+ scopes: ["User.Read.All"]
3679
+ })
3680
+ ];
3681
+ const graphClient = createMockGraphClient([
3682
+ { content: [{ type: "text", text: JSON.stringify({ id: "1" }) }] }
3683
+ ]);
3684
+ const server = createMockServer();
3685
+ const { registerGraphTools } = await loadModule();
3686
+ registerGraphTools(server, graphClient, { userFields: "id,displayName" });
3687
+ await server.tools.get(endpoint.alias).handler({
3688
+ ...endpoint.args,
3689
+ expand: "manager($select=displayName,jobTitle)"
3690
+ });
3691
+ expect(graphClient.graphRequest.mock.calls[0][0]).not.toContain("$expand");
3692
+ });
3693
+ it("restricts a graph-batch subrequest that reads the users surface", async () => {
3694
+ mockEndpoints.push(
3695
+ makeEndpoint({
3696
+ alias: "graph-batch",
3697
+ method: "post",
3698
+ path: "/$batch",
3699
+ parameters: [{ name: "body", type: "Body", schema: z.any() }]
3700
+ })
3701
+ );
3702
+ mockEndpointsJson = [
3703
+ makeConfig({ toolName: "graph-batch", pathPattern: "/$batch", method: "post" })
3704
+ ];
3705
+ const graphClient = createMockGraphClient([
3706
+ {
3707
+ content: [
3708
+ {
3709
+ type: "text",
3710
+ text: JSON.stringify({
3711
+ responses: [
3712
+ {
3713
+ id: "1",
3714
+ status: 200,
3715
+ body: {
3716
+ value: [{ id: "1", displayName: "Carlos", employeeId: "E-1" }]
3717
+ }
3718
+ },
3719
+ { id: "2", status: 200, body: { id: "m1", subject: "Keep me" } },
3720
+ { id: "3", status: 200, body: { id: "g1", displayName: "Team" } }
3721
+ ]
3722
+ })
3723
+ }
3724
+ ]
3725
+ }
3726
+ ]);
3727
+ const server = createMockServer();
3728
+ const { registerGraphTools } = await loadModule();
3729
+ registerGraphTools(server, graphClient, { userFields: "id,displayName" });
3730
+ const result = await server.tools.get("graph-batch").handler({
3731
+ body: {
3732
+ requests: [
3733
+ { id: "1", method: "GET", url: "/users?$select=id,displayName,employeeId" },
3734
+ { id: "2", method: "GET", url: "/me/messages/m1?$select=id,subject" },
3735
+ { id: "3", method: "GET", url: "/groups/g1?$expand=members($select=mail)" }
3736
+ ]
3737
+ }
3738
+ });
3739
+ const sentBody = JSON.parse(graphClient.graphRequest.mock.calls[0][1].body);
3740
+ expect(sentBody.requests[0].url).toBe("/users?$select=id,displayName");
3741
+ expect(sentBody.requests[1].url).toBe("/me/messages/m1?$select=id,subject");
3742
+ expect(sentBody.requests[2].url).toBe("/groups/g1");
3743
+ const responses = JSON.parse(result.content[0].text).responses;
3744
+ expect(responses[0].body.value[0]).toEqual({ id: "1", displayName: "Carlos" });
3745
+ expect(responses[1].body).toEqual({ id: "m1", subject: "Keep me" });
3746
+ });
3747
+ it("refuses a byte-passthrough read of the users surface", async () => {
3748
+ mockEndpoints.length = 0;
3749
+ mockEndpointsJson = [];
3750
+ const graphClient = createMockGraphClient();
3751
+ const server = createMockServer();
3752
+ const { registerGraphTools } = await loadModule();
3753
+ registerGraphTools(server, graphClient, { userFields: "id,displayName" });
3754
+ const result = await server.tools.get("download-bytes").handler({
3755
+ target: "/users?$select=employeeId"
3756
+ });
3757
+ expect(result.isError).toBe(true);
3758
+ expect(JSON.parse(result.content[0].text).error).toBe("user_fields_restricted");
3759
+ expect(graphClient.graphRequest).not.toHaveBeenCalled();
3760
+ });
3761
+ it("still allows byte reads below a user, such as a profile photo", async () => {
3762
+ mockEndpoints.length = 0;
3763
+ mockEndpointsJson = [];
3764
+ const graphClient = createMockGraphClient([
3765
+ { content: [{ type: "text", text: JSON.stringify({ message: "OK!" }) }] }
3766
+ ]);
3767
+ const server = createMockServer();
3768
+ const { registerGraphTools } = await loadModule();
3769
+ registerGraphTools(server, graphClient, { userFields: "id,displayName" });
3770
+ await server.tools.get("download-bytes").handler({
3771
+ target: "/users/abc/photo/$value"
3772
+ });
3773
+ expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
3774
+ });
3488
3775
  it("leaves the body untouched when no select was passed", async () => {
3489
3776
  const { result } = await run({});
3490
3777
  expect(JSON.parse(result.content[0].text)).toEqual(untrimmed);
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@ import { Command, Option } from "commander";
2
2
  import { readFileSync } from "fs";
3
3
  import path from "path";
4
4
  import { fileURLToPath } from "url";
5
- import { getCombinedPresetPattern, listPresets, presetRequiresOrgMode } from "./tool-categories.js";
6
5
  import { assertSignoffMarkersVisible } from "./lib/message-signoff.js";
6
+ import { getCombinedPresetPattern, listPresets, presetRequiresOrgMode } from "./tool-categories.js";
7
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
8
  const packageJsonPath = path.join(__dirname, "..", "package.json");
9
9
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
@@ -42,6 +42,9 @@ program.name("ms-365-mcp-server").description("Microsoft 365 MCP Server").versio
42
42
  ).option(
43
43
  "--allowed-scopes <scopes>",
44
44
  "Limit exposed tools to Graph scopes covered by this whitespace-separated allowlist"
45
+ ).option(
46
+ "--user-fields <fields>",
47
+ "Allow only these comma-separated Microsoft Graph fields in user profile responses (/users and /users/{id})"
45
48
  ).option(
46
49
  "--extra-scopes <scopes>",
47
50
  "Append additional Graph scopes (whitespace-separated) to the token request, beyond those derived from enabled tools. Use with your own app registration (MS365_MCP_CLIENT_ID/SECRET) to request scopes the default app does not declare, then call the endpoints via graph-batch."
@@ -130,6 +133,15 @@ function parseArgs() {
130
133
  );
131
134
  process.exit(1);
132
135
  }
136
+ if (options.userFields === void 0 && process.env.MS365_MCP_USER_FIELDS !== void 0) {
137
+ options.userFields = process.env.MS365_MCP_USER_FIELDS;
138
+ }
139
+ if (options.userFields !== void 0 && String(options.userFields).split(",").every((field) => field.trim() === "")) {
140
+ console.error(
141
+ "Error: --user-fields / MS365_MCP_USER_FIELDS was provided but names no fields. Provide one or more comma-separated Graph fields, or omit it to disable the user profile field boundary."
142
+ );
143
+ process.exit(1);
144
+ }
133
145
  if (options.extraScopes === void 0 && process.env.MS365_MCP_EXTRA_SCOPES !== void 0) {
134
146
  options.extraScopes = process.env.MS365_MCP_EXTRA_SCOPES;
135
147
  }
@@ -748,6 +748,7 @@ const microsoft_graph_customEmojiFromIdentitySet = z.object({
748
748
  user: microsoft_graph_identity.optional()
749
749
  }).passthrough();
750
750
  const microsoft_graph_teamworkCustomEmoji = z.object({
751
+ id: z.string().describe("The unique identifier for an entity. Read-only.").optional(),
751
752
  contentBytes: z.string().describe(
752
753
  "The base64-encoded image content of the emoji. Supported formats include PNG and GIF."
753
754
  ).nullish(),