@softeria/ms-365-mcp-server 0.166.1 → 0.168.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.
@@ -4190,4 +4190,317 @@ describe("graph-tools", () => {
4190
4190
  expect(isDestructiveOperation("DELETE", { readOnly: true })).toBe(true);
4191
4191
  });
4192
4192
  });
4193
+ describe("rewriteMentionShorthand", () => {
4194
+ const U1 = "11111111-2222-3333-4444-555555555555";
4195
+ const U2 = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
4196
+ const html = (content, extra = {}) => ({
4197
+ body: { contentType: "html", content },
4198
+ ...extra
4199
+ });
4200
+ const mention = (id, userId, name) => ({
4201
+ id,
4202
+ mentionText: name,
4203
+ mentioned: { user: { id: userId, displayName: name, userIdentityType: "aadUser" } }
4204
+ });
4205
+ it("rewrites one tag and builds the mentions array", async () => {
4206
+ const { rewriteMentionShorthand } = await loadModule();
4207
+ expect(rewriteMentionShorthand(html(`Hi <at user-id="${U1}">Jane Doe</at>!`)).body).toEqual(
4208
+ html('Hi <at id="0">Jane Doe</at>!', { mentions: [mention(0, U1, "Jane Doe")] })
4209
+ );
4210
+ });
4211
+ it("numbers several tags in order, including a repeated user", async () => {
4212
+ const { rewriteMentionShorthand } = await loadModule();
4213
+ const out = rewriteMentionShorthand(
4214
+ html(
4215
+ `<at user-id="${U1}">Jane</at> <at user-id="${U2}">Bob</at> <at user-id="${U1}">Jane</at>`
4216
+ )
4217
+ ).body;
4218
+ expect(out.body.content).toBe(
4219
+ '<at id="0">Jane</at> <at id="1">Bob</at> <at id="2">Jane</at>'
4220
+ );
4221
+ expect(out.mentions.map((m) => m.id)).toEqual([0, 1, 2]);
4222
+ });
4223
+ it("starts above ids already in the content and mentions", async () => {
4224
+ const { rewriteMentionShorthand } = await loadModule();
4225
+ const input = html(`<at id="0">Old</at> <at user-id="${U1}">Jane</at>`, {
4226
+ mentions: [mention(0, U2, "Old")]
4227
+ });
4228
+ const out = rewriteMentionShorthand(input).body;
4229
+ expect(out.body.content).toBe('<at id="0">Old</at> <at id="1">Jane</at>');
4230
+ expect(out.mentions).toEqual([mention(0, U2, "Old"), mention(1, U1, "Jane")]);
4231
+ });
4232
+ it("passes a correct hand-built body through unchanged", async () => {
4233
+ const { rewriteMentionShorthand } = await loadModule();
4234
+ const input = html('<at id="0">Jane</at>', { mentions: [mention(0, U1, "Jane")] });
4235
+ expect(rewriteMentionShorthand(input).body).toBe(input);
4236
+ });
4237
+ it("returns an error for a user-id that is not a GUID", async () => {
4238
+ const { rewriteMentionShorthand } = await loadModule();
4239
+ const input = html('<at user-id="jane@contoso.com">Jane</at>');
4240
+ const out = rewriteMentionShorthand(input);
4241
+ expect(out.error).toContain("jane@contoso.com");
4242
+ expect(out.body).toBe(input);
4243
+ });
4244
+ it("leaves non-html content without the shorthand alone", async () => {
4245
+ const { rewriteMentionShorthand } = await loadModule();
4246
+ const text = { body: { contentType: "text", content: "plain words" } };
4247
+ const bare = { body: { content: "plain words" } };
4248
+ expect(rewriteMentionShorthand(text).body).toBe(text);
4249
+ expect(rewriteMentionShorthand(bare).body).toBe(bare);
4250
+ });
4251
+ it.each([
4252
+ ["text", { contentType: "text" }],
4253
+ ["missing", {}]
4254
+ ])("returns an error for the shorthand when contentType is %s", async (_label, type) => {
4255
+ const { rewriteMentionShorthand } = await loadModule();
4256
+ const input = { body: { ...type, content: `<at user-id="${U1}">Jane</at>` } };
4257
+ const out = rewriteMentionShorthand(input);
4258
+ expect(out.error).toContain("contentType");
4259
+ expect(out.body).toBe(input);
4260
+ });
4261
+ it.each([
4262
+ ["single-quoted user-id", `<at user-id='${U1}'>Jane</at>`],
4263
+ ["spaces around =", `<at user-id = "${U1}">Jane</at>`],
4264
+ ["an extra attribute", `<at user-id="${U1}" class="x">Jane</at>`],
4265
+ ["nested markup in the name", `<at user-id="${U1}"><b>Jane</b></at>`]
4266
+ ])("returns an error for an unsupported tag form: %s", async (_label, tag) => {
4267
+ const { rewriteMentionShorthand } = await loadModule();
4268
+ const input = html(`Hi ${tag}`);
4269
+ const out = rewriteMentionShorthand(input);
4270
+ expect(out.error).toContain("Unsupported mention tag form");
4271
+ expect(out.body).toBe(input);
4272
+ });
4273
+ it("leaves a raw string body alone", async () => {
4274
+ const { rewriteMentionShorthand } = await loadModule();
4275
+ expect(rewriteMentionShorthand('{"body":{}}').body).toBe('{"body":{}}');
4276
+ expect(rewriteMentionShorthand(null).body).toBeNull();
4277
+ });
4278
+ it("keeps the display name verbatim", async () => {
4279
+ const { rewriteMentionShorthand } = await loadModule();
4280
+ const out = rewriteMentionShorthand(html(`<at user-id="${U1}">Smith &amp; Co</at>`)).body;
4281
+ expect(out.body.content).toBe('<at id="0">Smith &amp; Co</at>');
4282
+ expect(out.mentions[0].mentionText).toBe("Smith &amp; Co");
4283
+ });
4284
+ });
4285
+ describe("mention shorthand (tool wiring)", () => {
4286
+ const U1 = "11111111-2222-3333-4444-555555555555";
4287
+ const aliases = [
4288
+ ["send-channel-message", "post"],
4289
+ ["reply-to-channel-message", "post"],
4290
+ ["update-channel-message", "patch"],
4291
+ ["update-channel-message-reply", "patch"],
4292
+ ["send-chat-message", "post"],
4293
+ ["reply-to-chat-message", "post"],
4294
+ ["update-chat-message", "patch"]
4295
+ ];
4296
+ async function call(alias, method, body) {
4297
+ mockEndpoints.push(
4298
+ makeEndpoint({
4299
+ alias,
4300
+ method,
4301
+ path: "/chats/{chat-id}/messages",
4302
+ parameters: [{ name: "body", type: "Body", schema: z.object({}).passthrough() }]
4303
+ })
4304
+ );
4305
+ mockEndpointsJson = [
4306
+ makeConfig({ toolName: alias, method, pathPattern: "/chats/{chat-id}/messages" })
4307
+ ];
4308
+ const graphClient = createMockGraphClient([
4309
+ { content: [{ type: "text", text: JSON.stringify({ id: "m1" }) }] }
4310
+ ]);
4311
+ const server = createMockServer();
4312
+ const { registerGraphTools } = await loadModule();
4313
+ registerGraphTools(server, graphClient);
4314
+ const result = await server.tools.get(alias).handler({ "chat-id": "c1", body, confirm: true });
4315
+ return { result, graphClient };
4316
+ }
4317
+ const sent = (graphClient) => JSON.parse(graphClient.graphRequest.mock.calls[0][1].body);
4318
+ it.each(aliases)("rewrites the shorthand on %s", async (alias, method) => {
4319
+ const { graphClient } = await call(alias, method, {
4320
+ body: { contentType: "html", content: `Hi <at user-id="${U1}">Jane</at>` }
4321
+ });
4322
+ const out = sent(graphClient);
4323
+ expect(out.body.content).toBe('Hi <at id="0">Jane</at>');
4324
+ expect(out.mentions).toEqual([
4325
+ {
4326
+ id: 0,
4327
+ mentionText: "Jane",
4328
+ mentioned: { user: { id: U1, displayName: "Jane", userIdentityType: "aadUser" } }
4329
+ }
4330
+ ]);
4331
+ });
4332
+ it("returns a tool error and sends nothing for a bad user-id", async () => {
4333
+ const { result, graphClient } = await call("send-chat-message", "post", {
4334
+ body: { contentType: "html", content: '<at user-id="jane">Jane</at>' }
4335
+ });
4336
+ expect(result.isError).toBe(true);
4337
+ expect(result.content[0].text).toContain("not an Entra object id");
4338
+ expect(graphClient.graphRequest).not.toHaveBeenCalled();
4339
+ });
4340
+ it("does not rewrite on a tool outside the seven", async () => {
4341
+ const { graphClient } = await call("create-draft-email", "post", {
4342
+ body: { contentType: "html", content: `<at user-id="${U1}">Jane</at>` }
4343
+ });
4344
+ expect(sent(graphClient).body.content).toBe(`<at user-id="${U1}">Jane</at>`);
4345
+ });
4346
+ it("lists only tools that exist in endpoints.json and documents the shorthand", async () => {
4347
+ const actualFs = await vi.importActual("fs");
4348
+ const endpoints = JSON.parse(
4349
+ actualFs.readFileSync(join(__dirname, "..", "endpoints.json"), "utf8")
4350
+ );
4351
+ for (const [alias] of aliases) {
4352
+ const entry = endpoints.find((e) => e.toolName === alias);
4353
+ expect(entry, alias).toBeDefined();
4354
+ const tip = entry.llmTip;
4355
+ expect(tip, alias).toContain("user-id=");
4356
+ expect(tip, alias).not.toContain("name param");
4357
+ expect(tip, alias).toContain("userId");
4358
+ expect(tip.split("To @mention someone").length - 1, alias).toBe(1);
4359
+ }
4360
+ });
4361
+ });
4362
+ describe("filterMembersByName", () => {
4363
+ const body = {
4364
+ value: [
4365
+ { id: "1", displayName: "Jane Doe", email: "jane@contoso.com" },
4366
+ { id: "2", displayName: "John Smith", email: "jsmith@contoso.com" },
4367
+ { id: "3", displayName: null, email: null }
4368
+ ],
4369
+ "@odata.count": 3
4370
+ };
4371
+ const ids = (r) => r.value.map((m) => m.id);
4372
+ it("matches displayName case-insensitively", async () => {
4373
+ const { filterMembersByName } = await loadModule();
4374
+ expect(ids(filterMembersByName(body, "JANE"))).toEqual(["1"]);
4375
+ });
4376
+ it("matches email", async () => {
4377
+ const { filterMembersByName } = await loadModule();
4378
+ expect(ids(filterMembersByName(body, "jsmith@"))).toEqual(["2"]);
4379
+ });
4380
+ it("returns an empty value array when nothing matches", async () => {
4381
+ const { filterMembersByName } = await loadModule();
4382
+ expect(ids(filterMembersByName(body, "zzz"))).toEqual([]);
4383
+ });
4384
+ it("returns everything for an empty or whitespace-only name", async () => {
4385
+ const { filterMembersByName } = await loadModule();
4386
+ expect(ids(filterMembersByName(body, " "))).toEqual(["1", "2", "3"]);
4387
+ });
4388
+ it("does not match or throw on null fields", async () => {
4389
+ const { filterMembersByName } = await loadModule();
4390
+ expect(ids(filterMembersByName(body, "null"))).toEqual([]);
4391
+ });
4392
+ it("keeps other top-level fields", async () => {
4393
+ const { filterMembersByName } = await loadModule();
4394
+ expect(filterMembersByName(body, "jane")["@odata.count"]).toBe(3);
4395
+ });
4396
+ it("returns non-collection input unchanged", async () => {
4397
+ const { filterMembersByName } = await loadModule();
4398
+ expect(filterMembersByName("plain", "x")).toBe("plain");
4399
+ expect(filterMembersByName({ id: "1" }, "x")).toEqual({ id: "1" });
4400
+ });
4401
+ });
4402
+ describe("member name filter (tool wiring)", () => {
4403
+ const membersBody = {
4404
+ value: [
4405
+ { id: "1", displayName: "Jane Doe", email: "jane@contoso.com" },
4406
+ { id: "2", displayName: "John Smith", email: "jsmith@contoso.com" },
4407
+ { id: "3", displayName: null, email: null }
4408
+ ],
4409
+ "@odata.count": 3
4410
+ };
4411
+ const page = (b) => ({ content: [{ type: "text", text: JSON.stringify(b) }] });
4412
+ const ids = (result) => JSON.parse(result.content[0].text).value.map((m) => m.id);
4413
+ async function run(args, responses = [page(membersBody)], alias = "list-team-members", path = "/teams/{team-id}/members", outputFormat = "json") {
4414
+ mockEndpoints.push(makeEndpoint({ alias, path }));
4415
+ mockEndpointsJson = [makeConfig({ toolName: alias, pathPattern: path })];
4416
+ const graphClient = createMockGraphClient(responses, outputFormat);
4417
+ const server = createMockServer();
4418
+ const { registerGraphTools } = await loadModule();
4419
+ registerGraphTools(server, graphClient);
4420
+ const result = await server.tools.get(alias).handler({ "team-id": "t1", ...args });
4421
+ return { result, graphClient };
4422
+ }
4423
+ it("filters list-team-members by name", async () => {
4424
+ const { result } = await run({ name: "jane" });
4425
+ expect(ids(result)).toEqual(["1"]);
4426
+ });
4427
+ it("filters list-chat-members by email", async () => {
4428
+ const { result } = await run(
4429
+ { "chat-id": "c1", name: "jsmith" },
4430
+ [page(membersBody)],
4431
+ "list-chat-members",
4432
+ "/chats/{chat-id}/members"
4433
+ );
4434
+ expect(ids(result)).toEqual(["2"]);
4435
+ });
4436
+ it("does not send name to Graph", async () => {
4437
+ const { graphClient } = await run({ name: "jane" });
4438
+ expect(graphClient.graphRequest.mock.calls[0][0]).not.toContain("name");
4439
+ });
4440
+ it("returns the full list for a whitespace-only name", async () => {
4441
+ const { result } = await run({ name: " " });
4442
+ expect(ids(result)).toEqual(["1", "2", "3"]);
4443
+ });
4444
+ it("leaves an error response untouched", async () => {
4445
+ const err = { content: [{ type: "text", text: '{"error":"nope"}' }], isError: true };
4446
+ const { result } = await run({ name: "jane" }, [err]);
4447
+ expect(result.content[0].text).toBe('{"error":"nope"}');
4448
+ });
4449
+ it("keeps @odata.count as the unfiltered total", async () => {
4450
+ const { result } = await run({ name: "jane" });
4451
+ expect(JSON.parse(result.content[0].text)["@odata.count"]).toBe(3);
4452
+ });
4453
+ it("filters after fetchAllPages merges the pages", async () => {
4454
+ const { result } = await run({ name: "ann", fetchAllPages: true }, [
4455
+ page({
4456
+ value: [{ id: "1", displayName: "Ann", email: "a@x.com" }],
4457
+ "@odata.nextLink": "https://graph.microsoft.com/v1.0/teams/t1/members?$skiptoken=x"
4458
+ }),
4459
+ page({
4460
+ value: [
4461
+ { id: "2", displayName: "Bob", email: "b@x.com" },
4462
+ { id: "3", displayName: "Annette", email: "n@x.com" }
4463
+ ]
4464
+ })
4465
+ ]);
4466
+ expect(ids(result)).toEqual(["1", "3"]);
4467
+ });
4468
+ it("matches only fields that survive $select projection", async () => {
4469
+ const { result } = await run({ select: "id,displayName", name: "contoso" });
4470
+ expect(ids(result)).toEqual([]);
4471
+ });
4472
+ describe("under --toon", () => {
4473
+ const parseToon = (result) => JSON.parse(result.content[0].text.replace(/^TOON:/, ""));
4474
+ const toon = (args, responses = [page(membersBody)]) => run(args, responses, "list-team-members", "/teams/{team-id}/members", "toon");
4475
+ it("filters when no projection or merge applies", async () => {
4476
+ const { result } = await toon({ name: "jane" });
4477
+ expect(parseToon(result).value.map((m) => m.id)).toEqual(["1"]);
4478
+ });
4479
+ it("filters when $select projects the body", async () => {
4480
+ const { result } = await toon({ select: "id,displayName", name: "jane" });
4481
+ expect(parseToon(result).value).toEqual([{ id: "1", displayName: "Jane Doe" }]);
4482
+ });
4483
+ it("filters after fetchAllPages merges the pages", async () => {
4484
+ const { result } = await toon({ name: "ann", fetchAllPages: true }, [
4485
+ page({
4486
+ value: [{ id: "1", displayName: "Ann", email: "a@x.com" }],
4487
+ "@odata.nextLink": "https://graph.microsoft.com/v1.0/teams/t1/members?$skiptoken=x"
4488
+ }),
4489
+ page({
4490
+ value: [
4491
+ { id: "2", displayName: "Bob", email: "b@x.com" },
4492
+ { id: "3", displayName: "Annette", email: "n@x.com" }
4493
+ ]
4494
+ })
4495
+ ]);
4496
+ expect(parseToon(result).value.map((m) => m.id)).toEqual(["1", "3"]);
4497
+ });
4498
+ });
4499
+ it("still matches a projected field", async () => {
4500
+ const { result } = await run({ select: "id,displayName", name: "jane" });
4501
+ expect(JSON.parse(result.content[0].text).value).toEqual([
4502
+ { id: "1", displayName: "Jane Doe" }
4503
+ ]);
4504
+ });
4505
+ });
4193
4506
  });
@@ -1737,7 +1737,7 @@
1737
1737
  "toolName": "list-chat-members",
1738
1738
  "presets": ["teams", "work"],
1739
1739
  "workScopes": ["ChatMember.Read"],
1740
- "llmTip": "Lists members of a chat. Each member has displayName, email, roles (owner/guest), and visibleHistoryStartDateTime. For meeting chats, use the chatInfo.threadId from get-online-meeting as the chat-id to see who participated in the meeting chat."
1740
+ "llmTip": "Lists members of a chat. Each member has displayName, email, roles (owner/guest), and visibleHistoryStartDateTime. For meeting chats, use the chatInfo.threadId from get-online-meeting as the chat-id to see who participated in the meeting chat. Pass name to keep only members whose displayName or email contains that text (case-insensitive); use the returned userId in an @mention."
1741
1741
  },
1742
1742
  {
1743
1743
  "pathPattern": "/chats/{chat-id}/messages",
@@ -1759,7 +1759,8 @@
1759
1759
  "method": "patch",
1760
1760
  "toolName": "update-chat-message",
1761
1761
  "presets": ["teams", "work"],
1762
- "workScopes": ["Chat.ReadWrite"]
1762
+ "workScopes": ["Chat.ReadWrite"],
1763
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType html in the body."
1763
1764
  },
1764
1765
  {
1765
1766
  "pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/hostedContents",
@@ -1775,7 +1776,7 @@
1775
1776
  "toolName": "send-chat-message",
1776
1777
  "presets": ["teams", "teams-write", "work"],
1777
1778
  "workScopes": ["ChatMessage.Send"],
1778
- "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. To @mention someone, put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1779
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1779
1780
  },
1780
1781
  {
1781
1782
  "pathPattern": "/me/joinedTeams",
@@ -1878,7 +1879,8 @@
1878
1879
  "method": "patch",
1879
1880
  "toolName": "update-channel-message",
1880
1881
  "presets": ["teams", "work"],
1881
- "workScopes": ["ChannelMessage.ReadWrite"]
1882
+ "workScopes": ["ChannelMessage.ReadWrite"],
1883
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType html in the body."
1882
1884
  },
1883
1885
  {
1884
1886
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/hostedContents",
@@ -1894,7 +1896,7 @@
1894
1896
  "toolName": "send-channel-message",
1895
1897
  "presets": ["teams", "teams-write", "work"],
1896
1898
  "workScopes": ["ChannelMessage.Send"],
1897
- "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. To @mention someone, put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1899
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1898
1900
  },
1899
1901
  {
1900
1902
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
@@ -1902,7 +1904,7 @@
1902
1904
  "toolName": "reply-to-channel-message",
1903
1905
  "presets": ["teams", "teams-write", "work"],
1904
1906
  "workScopes": ["ChannelMessage.Send"],
1905
- "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. To @mention someone, put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1907
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1906
1908
  },
1907
1909
  {
1908
1910
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
@@ -1916,7 +1918,8 @@
1916
1918
  "method": "patch",
1917
1919
  "toolName": "update-channel-message-reply",
1918
1920
  "presets": ["teams", "work"],
1919
- "workScopes": ["ChannelMessage.ReadWrite"]
1921
+ "workScopes": ["ChannelMessage.ReadWrite"],
1922
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType html in the body."
1920
1923
  },
1921
1924
  {
1922
1925
  "pathPattern": "/teams/{team-id}/members",
@@ -1958,7 +1961,8 @@
1958
1961
  "method": "get",
1959
1962
  "toolName": "list-team-members",
1960
1963
  "presets": ["teams", "work"],
1961
- "workScopes": ["TeamMember.Read.All"]
1964
+ "workScopes": ["TeamMember.Read.All"],
1965
+ "llmTip": "Lists members of a team. Each member has displayName, email, roles and userId. Pass name to keep only members whose displayName or email contains that text (case-insensitive); use the returned userId in an @mention."
1962
1966
  },
1963
1967
  {
1964
1968
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/filesFolder",
@@ -1981,7 +1985,7 @@
1981
1985
  "toolName": "reply-to-chat-message",
1982
1986
  "presets": ["teams", "work"],
1983
1987
  "workScopes": ["ChatMessage.Send"],
1984
- "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. To @mention someone, put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1988
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1985
1989
  },
1986
1990
  {
1987
1991
  "pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/setReaction",
@@ -203,6 +203,17 @@ function rewriteMailSearchQuotes(expr) {
203
203
  }
204
204
  return balanceTrailingSlashes(out.trim());
205
205
  }
206
+ const MEMBER_NAME_FILTER_TOOLS = /* @__PURE__ */ new Set(["list-team-members", "list-chat-members"]);
207
+ const MEMBER_NAME_PARAM_DESCRIPTION = "Case-insensitive text to match against each member's displayName and email. Applied after $select projection, so only fields you can see are matched. Without fetchAllPages only the fetched page is filtered, and @odata.count still reports the unfiltered total.";
208
+ function filterMembersByName(parsed, name) {
209
+ const needle = name.trim().toLowerCase();
210
+ if (needle === "" || !isPlainObject(parsed) || !Array.isArray(parsed.value)) return parsed;
211
+ const matches = (member) => isPlainObject(member) && ["displayName", "email"].some((field) => {
212
+ const value = member[field];
213
+ return typeof value === "string" && value.toLowerCase().includes(needle);
214
+ });
215
+ return { ...parsed, value: parsed.value.filter(matches) };
216
+ }
206
217
  function normalizeSearchQueryParam(queryParams, path2, toolAlias) {
207
218
  if (!isOutlookMailPath(path2)) return;
208
219
  const raw = queryParams["$search"];
@@ -386,6 +397,63 @@ function collectRecipients(node, domains, counter, depth = 0) {
386
397
  collectRecipients(value, domains, counter, depth + 1);
387
398
  }
388
399
  }
400
+ const MENTION_SHORTHAND_TOOLS = /* @__PURE__ */ new Set([
401
+ "send-channel-message",
402
+ "reply-to-channel-message",
403
+ "update-channel-message",
404
+ "update-channel-message-reply",
405
+ "send-chat-message",
406
+ "reply-to-chat-message",
407
+ "update-chat-message"
408
+ ]);
409
+ const AAD_GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
410
+ const SHORTHAND_AT = /<at\s+user-id="([^"]*)">([^<]*)<\/at>/gi;
411
+ const HAS_SHORTHAND = /<at\b[^>]*\buser-id\b/i;
412
+ function rewriteMentionShorthand(body) {
413
+ if (!isPlainObject(body) || !isPlainObject(body.body)) return { body };
414
+ const itemBody = body.body;
415
+ const content = itemBody.content;
416
+ if (typeof content !== "string" || !HAS_SHORTHAND.test(content)) return { body };
417
+ if (String(itemBody.contentType).toLowerCase() !== "html") {
418
+ return {
419
+ body,
420
+ error: 'The <at user-id> mention shorthand needs contentType "html" on the message body. Set body.contentType to "html".'
421
+ };
422
+ }
423
+ const existing = Array.isArray(body.mentions) ? body.mentions : [];
424
+ let nextId = 0;
425
+ for (const m of existing) {
426
+ if (isPlainObject(m) && typeof m.id === "number") nextId = Math.max(nextId, m.id + 1);
427
+ }
428
+ for (const m of content.matchAll(/<at\s+id="(\d+)"/gi)) {
429
+ nextId = Math.max(nextId, Number(m[1]) + 1);
430
+ }
431
+ const added = [];
432
+ let error;
433
+ const rewritten = content.replace(SHORTHAND_AT, (tag, userId, name) => {
434
+ if (!AAD_GUID.test(userId)) {
435
+ error ?? (error = `Mention user-id "${userId}" is not an Entra object id (GUID). Use the userId from list-team-members or list-chat-members.`);
436
+ return tag;
437
+ }
438
+ const id = nextId++;
439
+ added.push({
440
+ id,
441
+ mentionText: name,
442
+ mentioned: { user: { id: userId, displayName: name, userIdentityType: "aadUser" } }
443
+ });
444
+ return `<at id="${id}">${name}</at>`;
445
+ });
446
+ if (error) return { body, error };
447
+ if (HAS_SHORTHAND.test(rewritten)) {
448
+ return {
449
+ body,
450
+ error: 'Unsupported mention tag form. Write <at user-id="<guid>">Name</at> with double quotes, no other attributes and plain text inside.'
451
+ };
452
+ }
453
+ return {
454
+ body: { ...body, body: { ...itemBody, content: rewritten }, mentions: [...existing, ...added] }
455
+ };
456
+ }
389
457
  function bodyForRecipientWalk(body) {
390
458
  if (typeof body !== "string") return body;
391
459
  if (!/^\s*[{[]/.test(body)) return void 0;
@@ -1541,8 +1609,10 @@ async function executeGraphTool(tool, config, graphClient, params, authManager,
1541
1609
  parameterDefinitions.find((p) => p.type === "Body")?.schema
1542
1610
  );
1543
1611
  const strayBodyFields = {};
1612
+ const memberNameFilter = MEMBER_NAME_FILTER_TOOLS.has(tool.alias) && typeof params.name === "string" ? params.name : void 0;
1544
1613
  for (const [paramName, paramValue] of Object.entries(params)) {
1545
1614
  if (paramName === "fileName" && config?.blobOutput) continue;
1615
+ if (paramName === "name" && MEMBER_NAME_FILTER_TOOLS.has(tool.alias)) continue;
1546
1616
  if ([
1547
1617
  "account",
1548
1618
  "confirm",
@@ -1717,6 +1787,16 @@ async function executeGraphTool(tool, config, graphClient, params, authManager,
1717
1787
  );
1718
1788
  }
1719
1789
  }
1790
+ if (MENTION_SHORTHAND_TOOLS.has(tool.alias)) {
1791
+ const rewritten = rewriteMentionShorthand(body);
1792
+ if (rewritten.error) {
1793
+ return {
1794
+ content: [{ type: "text", text: JSON.stringify({ error: rewritten.error }) }],
1795
+ isError: true
1796
+ };
1797
+ }
1798
+ body = rewritten.body;
1799
+ }
1720
1800
  if (TOP_UNSUPPORTED_DELTA_TOOLS.has(tool.alias)) {
1721
1801
  delete queryParams["$top"];
1722
1802
  }
@@ -1848,12 +1928,23 @@ async function executeGraphTool(tool, config, graphClient, params, authManager,
1848
1928
  const mergePages = fetchAllPages && paginationEnabled;
1849
1929
  const willProject = (requestedSelect.length > 0 || isUserFieldBoundary) && params.excludeResponse !== true;
1850
1930
  const willProjectBatch = batchRestricted.size > 0 && params.excludeResponse !== true;
1851
- if (mergePages || willProject || willProjectBatch) {
1931
+ const willFilterMembers = memberNameFilter !== void 0 && memberNameFilter.trim() !== "" && params.excludeResponse !== true;
1932
+ if (mergePages || willProject || willProjectBatch || willFilterMembers) {
1852
1933
  options.forceJsonOutput = true;
1853
1934
  }
1854
1935
  let response = await graphClient.graphRequest(path2, options);
1855
1936
  const shouldProject = willProject && !response?.isError;
1856
1937
  let projectionHandled = false;
1938
+ let memberFilterHandled = false;
1939
+ const applyMemberFilter = (body2) => {
1940
+ if (!willFilterMembers) return body2;
1941
+ memberFilterHandled = true;
1942
+ const filtered = filterMembersByName(body2, memberNameFilter);
1943
+ if (isPlainObject(filtered) && Array.isArray(filtered.value) && response._meta) {
1944
+ response._meta = { ...response._meta, result_count: filtered.value.length };
1945
+ }
1946
+ return filtered;
1947
+ };
1857
1948
  const applyProjection = (body2) => {
1858
1949
  if (isTransportEnvelope(body2)) {
1859
1950
  logger.info("Skipping $select projection: body is a transport envelope, not a resource");
@@ -1948,7 +2039,9 @@ async function executeGraphTool(tool, config, graphClient, params, authManager,
1948
2039
  logger.error(`Error during pagination: ${e}`);
1949
2040
  }
1950
2041
  if (combinedResponse !== void 0) {
1951
- const merged = shouldProject ? applyProjection(combinedResponse) : combinedResponse;
2042
+ const merged = applyMemberFilter(
2043
+ shouldProject ? applyProjection(combinedResponse) : combinedResponse
2044
+ );
1952
2045
  projectionHandled = shouldProject;
1953
2046
  response.content[0].text = graphClient.serialize(merged);
1954
2047
  }
@@ -1956,7 +2049,9 @@ async function executeGraphTool(tool, config, graphClient, params, authManager,
1956
2049
  if (shouldProject && !projectionHandled && !response?.isError && response?.content?.[0]?.text) {
1957
2050
  try {
1958
2051
  const parsed = JSON.parse(response.content[0].text);
1959
- response.content[0].text = graphClient.serialize(applyProjection(parsed));
2052
+ response.content[0].text = graphClient.serialize(
2053
+ applyMemberFilter(applyProjection(parsed))
2054
+ );
1960
2055
  } catch {
1961
2056
  }
1962
2057
  }
@@ -1969,6 +2064,14 @@ async function executeGraphTool(tool, config, graphClient, params, authManager,
1969
2064
  } catch {
1970
2065
  }
1971
2066
  }
2067
+ if (willFilterMembers && !memberFilterHandled && !response?.isError && response?.content?.[0]?.text) {
2068
+ try {
2069
+ response.content[0].text = graphClient.serialize(
2070
+ applyMemberFilter(JSON.parse(response.content[0].text))
2071
+ );
2072
+ } catch {
2073
+ }
2074
+ }
1972
2075
  if (response?.content?.[0]?.text) {
1973
2076
  const responseText = response.content[0].text;
1974
2077
  logger.info(`Response size: ${responseText.length} characters`);
@@ -2125,6 +2228,9 @@ function registerGraphTools(server, graphClient, options = {}) {
2125
2228
  const maxPages = getMaxPages();
2126
2229
  paramSchema["fetchAllPages"] = z.boolean().describe(getFetchAllPagesParamDescription(maxPages, tool.alias)).optional();
2127
2230
  }
2231
+ if (MEMBER_NAME_FILTER_TOOLS.has(tool.alias)) {
2232
+ paramSchema["name"] = z.string().describe(MEMBER_NAME_PARAM_DESCRIPTION).optional();
2233
+ }
2128
2234
  if (isSkiptokenApplicable(tool, Object.keys(paramSchema))) {
2129
2235
  paramSchema["skiptoken"] = z.string().describe(SKIPTOKEN_PARAM_DESCRIPTION).optional();
2130
2236
  }
@@ -2545,8 +2651,10 @@ export {
2545
2651
  UTILITY_TOOLS,
2546
2652
  buildDiscoverySearchIndex,
2547
2653
  buildToolsRegistry,
2654
+ filterMembersByName,
2548
2655
  isDestructiveOperation,
2549
2656
  registerDiscoveryTools,
2550
2657
  registerGraphTools,
2658
+ rewriteMentionShorthand,
2551
2659
  scoreDiscoveryQuery
2552
2660
  };
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.166.1",
4
+ "version": "0.168.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",
@@ -1737,7 +1737,7 @@
1737
1737
  "toolName": "list-chat-members",
1738
1738
  "presets": ["teams", "work"],
1739
1739
  "workScopes": ["ChatMember.Read"],
1740
- "llmTip": "Lists members of a chat. Each member has displayName, email, roles (owner/guest), and visibleHistoryStartDateTime. For meeting chats, use the chatInfo.threadId from get-online-meeting as the chat-id to see who participated in the meeting chat."
1740
+ "llmTip": "Lists members of a chat. Each member has displayName, email, roles (owner/guest), and visibleHistoryStartDateTime. For meeting chats, use the chatInfo.threadId from get-online-meeting as the chat-id to see who participated in the meeting chat. Pass name to keep only members whose displayName or email contains that text (case-insensitive); use the returned userId in an @mention."
1741
1741
  },
1742
1742
  {
1743
1743
  "pathPattern": "/chats/{chat-id}/messages",
@@ -1759,7 +1759,8 @@
1759
1759
  "method": "patch",
1760
1760
  "toolName": "update-chat-message",
1761
1761
  "presets": ["teams", "work"],
1762
- "workScopes": ["Chat.ReadWrite"]
1762
+ "workScopes": ["Chat.ReadWrite"],
1763
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType html in the body."
1763
1764
  },
1764
1765
  {
1765
1766
  "pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/hostedContents",
@@ -1775,7 +1776,7 @@
1775
1776
  "toolName": "send-chat-message",
1776
1777
  "presets": ["teams", "teams-write", "work"],
1777
1778
  "workScopes": ["ChatMessage.Send"],
1778
- "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. To @mention someone, put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1779
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1779
1780
  },
1780
1781
  {
1781
1782
  "pathPattern": "/me/joinedTeams",
@@ -1878,7 +1879,8 @@
1878
1879
  "method": "patch",
1879
1880
  "toolName": "update-channel-message",
1880
1881
  "presets": ["teams", "work"],
1881
- "workScopes": ["ChannelMessage.ReadWrite"]
1882
+ "workScopes": ["ChannelMessage.ReadWrite"],
1883
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType html in the body."
1882
1884
  },
1883
1885
  {
1884
1886
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/hostedContents",
@@ -1894,7 +1896,7 @@
1894
1896
  "toolName": "send-channel-message",
1895
1897
  "presets": ["teams", "teams-write", "work"],
1896
1898
  "workScopes": ["ChannelMessage.Send"],
1897
- "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. To @mention someone, put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1899
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1898
1900
  },
1899
1901
  {
1900
1902
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
@@ -1902,7 +1904,7 @@
1902
1904
  "toolName": "reply-to-channel-message",
1903
1905
  "presets": ["teams", "teams-write", "work"],
1904
1906
  "workScopes": ["ChannelMessage.Send"],
1905
- "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. To @mention someone, put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1907
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1906
1908
  },
1907
1909
  {
1908
1910
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
@@ -1916,7 +1918,8 @@
1916
1918
  "method": "patch",
1917
1919
  "toolName": "update-channel-message-reply",
1918
1920
  "presets": ["teams", "work"],
1919
- "workScopes": ["ChannelMessage.ReadWrite"]
1921
+ "workScopes": ["ChannelMessage.ReadWrite"],
1922
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType html in the body."
1920
1923
  },
1921
1924
  {
1922
1925
  "pathPattern": "/teams/{team-id}/members",
@@ -1958,7 +1961,8 @@
1958
1961
  "method": "get",
1959
1962
  "toolName": "list-team-members",
1960
1963
  "presets": ["teams", "work"],
1961
- "workScopes": ["TeamMember.Read.All"]
1964
+ "workScopes": ["TeamMember.Read.All"],
1965
+ "llmTip": "Lists members of a team. Each member has displayName, email, roles and userId. Pass name to keep only members whose displayName or email contains that text (case-insensitive); use the returned userId in an @mention."
1962
1966
  },
1963
1967
  {
1964
1968
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/filesFolder",
@@ -1981,7 +1985,7 @@
1981
1985
  "toolName": "reply-to-chat-message",
1982
1986
  "presets": ["teams", "work"],
1983
1987
  "workScopes": ["ChatMessage.Send"],
1984
- "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. To @mention someone, put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1988
+ "llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body — plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
1985
1989
  },
1986
1990
  {
1987
1991
  "pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/setReaction",