@softeria/ms-365-mcp-server 0.129.0 → 0.130.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.
@@ -62,7 +62,7 @@ function makeConfig(overrides = {}) {
62
62
  ...overrides
63
63
  };
64
64
  }
65
- function createMockGraphClient(responses) {
65
+ function createMockGraphClient(responses, outputFormat = "json") {
66
66
  const responseQueue = [...responses || []];
67
67
  return {
68
68
  graphRequest: vi.fn().mockImplementation(async () => {
@@ -72,7 +72,12 @@ function createMockGraphClient(responses) {
72
72
  return {
73
73
  content: [{ type: "text", text: JSON.stringify({ value: [] }) }]
74
74
  };
75
- })
75
+ }),
76
+ // Fake serialize: prefix the JSON in toon mode so a test can tell the merged
77
+ // body went through serialize() and not a plain JSON.stringify.
78
+ serialize: vi.fn().mockImplementation(
79
+ (data) => outputFormat === "toon" ? `TOON:${JSON.stringify(data)}` : JSON.stringify(data)
80
+ )
76
81
  };
77
82
  }
78
83
  async function loadModule() {
@@ -161,6 +166,61 @@ describe("graph-tools", () => {
161
166
  expect(parsed.value.map((v) => v.id)).toEqual(["1", "2", "3"]);
162
167
  expect(parsed["@odata.nextLink"]).toBeUndefined();
163
168
  });
169
+ it("merges all pages under --toon and encodes the combined result once (#560)", async () => {
170
+ const endpoint = makeEndpoint();
171
+ const config = makeConfig();
172
+ mockEndpoints.push(endpoint);
173
+ mockEndpointsJson = [config];
174
+ const graphClient = createMockGraphClient(
175
+ [
176
+ {
177
+ content: [
178
+ {
179
+ type: "text",
180
+ text: JSON.stringify({
181
+ value: [{ id: "1" }, { id: "2" }],
182
+ "@odata.nextLink": "https://graph.microsoft.com/v1.0/me/messages?$skip=2"
183
+ })
184
+ }
185
+ ]
186
+ },
187
+ {
188
+ content: [{ type: "text", text: JSON.stringify({ value: [{ id: "3" }] }) }]
189
+ }
190
+ ],
191
+ "toon"
192
+ );
193
+ const server = createMockServer();
194
+ const { registerGraphTools } = await loadModule();
195
+ registerGraphTools(server, graphClient);
196
+ const tool = server.tools.get("test-tool");
197
+ const result = await tool.handler({ fetchAllPages: true });
198
+ for (const call of graphClient.graphRequest.mock.calls) {
199
+ expect(call[1]?.forceJsonOutput).toBe(true);
200
+ }
201
+ expect(graphClient.serialize).toHaveBeenCalledTimes(1);
202
+ expect(result.content[0].text.startsWith("TOON:")).toBe(true);
203
+ const parsed = JSON.parse(result.content[0].text.slice("TOON:".length));
204
+ expect(parsed.value.map((v) => v.id)).toEqual(["1", "2", "3"]);
205
+ expect(parsed["@odata.nextLink"]).toBeUndefined();
206
+ });
207
+ it("does not inject value:[] when fetchAllPages hits a single-object (non-collection) GET", async () => {
208
+ const endpoint = makeEndpoint();
209
+ const config = makeConfig();
210
+ mockEndpoints.push(endpoint);
211
+ mockEndpointsJson = [config];
212
+ const graphClient = createMockGraphClient([
213
+ { content: [{ type: "text", text: JSON.stringify({ id: "abc", displayName: "Solo" }) }] }
214
+ ]);
215
+ const server = createMockServer();
216
+ const { registerGraphTools } = await loadModule();
217
+ registerGraphTools(server, graphClient);
218
+ const result = await server.tools.get("test-tool").handler({ fetchAllPages: true });
219
+ const parsed = JSON.parse(result.content[0].text);
220
+ expect(parsed).toEqual({ id: "abc", displayName: "Solo" });
221
+ expect(parsed.value).toBeUndefined();
222
+ expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
223
+ });
164
224
  it("should stop at 100 page limit", async () => {
165
225
  const endpoint = makeEndpoint();
166
226
  const config = makeConfig();
@@ -712,6 +772,27 @@ describe("graph-tools", () => {
712
772
  expect(payload.size).toBe(12727);
713
773
  expect(payload.contentType).toBe("application/pdf");
714
774
  });
775
+ it("forces a JSON body on the metadata request so it works under --toon (#560)", async () => {
776
+ mockEndpoints.length = 0;
777
+ mockEndpointsJson = [];
778
+ const graphClient = {
779
+ graphRequest: vi.fn().mockResolvedValue({
780
+ content: [
781
+ {
782
+ type: "text",
783
+ text: JSON.stringify({ "@microsoft.graph.downloadUrl": "https://dl.example/x" })
784
+ }
785
+ ]
786
+ })
787
+ };
788
+ const server = createMockServer();
789
+ const { registerGraphTools } = await loadModule();
790
+ registerGraphTools(server, graphClient);
791
+ const result = await server.tools.get("get-download-url").handler({ target: "/drives/d1/items/item1/content" });
792
+ const [, opts] = graphClient.graphRequest.mock.calls[0];
793
+ expect(opts?.forceJsonOutput).toBe(true);
794
+ expect(JSON.parse(result.content[0].text).downloadUrl).toBe("https://dl.example/x");
795
+ });
715
796
  it("rejects query-shaped targets instead of silently changing request semantics", async () => {
716
797
  mockEndpoints.length = 0;
717
798
  mockEndpointsJson = [];
@@ -1393,6 +1393,13 @@
1393
1393
  "presets": ["teams", "work"],
1394
1394
  "workScopes": ["ChatMessage.Read"]
1395
1395
  },
1396
+ {
1397
+ "pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}",
1398
+ "method": "patch",
1399
+ "toolName": "update-chat-message",
1400
+ "presets": ["teams", "work"],
1401
+ "workScopes": ["Chat.ReadWrite"]
1402
+ },
1396
1403
  {
1397
1404
  "pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/hostedContents",
1398
1405
  "method": "get",
@@ -1472,6 +1479,13 @@
1472
1479
  "presets": ["teams", "work"],
1473
1480
  "workScopes": ["ChannelMessage.Read.All"]
1474
1481
  },
1482
+ {
1483
+ "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}",
1484
+ "method": "patch",
1485
+ "toolName": "update-channel-message",
1486
+ "presets": ["teams", "work"],
1487
+ "workScopes": ["ChannelMessage.ReadWrite"]
1488
+ },
1475
1489
  {
1476
1490
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/hostedContents",
1477
1491
  "method": "get",
@@ -1503,6 +1517,13 @@
1503
1517
  "presets": ["teams", "work"],
1504
1518
  "workScopes": ["ChannelMessage.Read.All"]
1505
1519
  },
1520
+ {
1521
+ "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies/{chatMessage-id1}",
1522
+ "method": "patch",
1523
+ "toolName": "update-channel-message-reply",
1524
+ "presets": ["teams", "work"],
1525
+ "workScopes": ["ChannelMessage.ReadWrite"]
1526
+ },
1506
1527
  {
1507
1528
  "pathPattern": "/teams/{team-id}/members",
1508
1529
  "method": "post",
@@ -1613,7 +1613,7 @@ const microsoft_graph_group = z.object({
1613
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})."
1614
1614
  ).nullish(),
1615
1615
  assignedLabels: z.array(microsoft_graph_assignedLabel).describe(
1616
- "The list of sensitivity label pairs (label ID, label name) associated with a Microsoft 365 group. Requires $select to retrieve. This property can be updated only in delegated scenarios where the caller requires both the Microsoft Graph permission and a supported administrator role."
1616
+ "The list of sensitivity label pairs (label ID, label name) associated with a Microsoft 365 group or a cloud security group. Requires a Microsoft Entra ID P1 license. Requires $select to retrieve. This property can be specified during group creation or update. However, for cloud security groups, it's immutable once set. This property can be updated only in delegated scenarios where the caller requires both the Microsoft Graph permission and a supported administrator role. See Key differences from Microsoft 365 group labeling to learn more about managing this property for Microsoft 365 vs. cloud security groups."
1617
1617
  ).optional(),
1618
1618
  assignedLicenses: z.array(microsoft_graph_assignedLicense).describe(
1619
1619
  "The licenses that are assigned to the group. Requires $select to retrieve. Supports $filter (eq). Read-only."
@@ -5497,6 +5497,22 @@ const endpoints = makeApi([
5497
5497
  ],
5498
5498
  response: z.void()
5499
5499
  },
5500
+ {
5501
+ method: "patch",
5502
+ path: "/chats/:chatId/messages/:chatMessageId",
5503
+ alias: "update-chat-message",
5504
+ description: `Update the navigation property messages in chats`,
5505
+ requestFormat: "json",
5506
+ parameters: [
5507
+ {
5508
+ name: "body",
5509
+ description: `New navigation property values`,
5510
+ type: "Body",
5511
+ schema: microsoft_graph_chatMessage
5512
+ }
5513
+ ],
5514
+ response: z.void()
5515
+ },
5500
5516
  {
5501
5517
  method: "get",
5502
5518
  path: "/chats/:chatId/messages/:chatMessageId/hostedContents",
@@ -6871,7 +6887,7 @@ You can search within a folder hierarchy, a whole drive, or files shared with th
6871
6887
  "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})."
6872
6888
  ).nullish(),
6873
6889
  assignedLabels: z.array(microsoft_graph_assignedLabel).describe(
6874
- "The list of sensitivity label pairs (label ID, label name) associated with a Microsoft 365 group. Requires $select to retrieve. This property can be updated only in delegated scenarios where the caller requires both the Microsoft Graph permission and a supported administrator role."
6890
+ "The list of sensitivity label pairs (label ID, label name) associated with a Microsoft 365 group or a cloud security group. Requires a Microsoft Entra ID P1 license. Requires $select to retrieve. This property can be specified during group creation or update. However, for cloud security groups, it's immutable once set. This property can be updated only in delegated scenarios where the caller requires both the Microsoft Graph permission and a supported administrator role. See Key differences from Microsoft 365 group labeling to learn more about managing this property for Microsoft 365 vs. cloud security groups."
6875
6891
  ).optional(),
6876
6892
  assignedLicenses: z.array(microsoft_graph_assignedLicense).describe(
6877
6893
  "The licenses that are assigned to the group. Requires $select to retrieve. Supports $filter (eq). Read-only."
@@ -6982,7 +6998,7 @@ You can create or update the following types of group: By default, this operatio
6982
6998
  "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})."
6983
6999
  ).nullish(),
6984
7000
  assignedLabels: z.array(microsoft_graph_assignedLabel).describe(
6985
- "The list of sensitivity label pairs (label ID, label name) associated with a Microsoft 365 group. Requires $select to retrieve. This property can be updated only in delegated scenarios where the caller requires both the Microsoft Graph permission and a supported administrator role."
7001
+ "The list of sensitivity label pairs (label ID, label name) associated with a Microsoft 365 group or a cloud security group. Requires a Microsoft Entra ID P1 license. Requires $select to retrieve. This property can be specified during group creation or update. However, for cloud security groups, it's immutable once set. This property can be updated only in delegated scenarios where the caller requires both the Microsoft Graph permission and a supported administrator role. See Key differences from Microsoft 365 group labeling to learn more about managing this property for Microsoft 365 vs. cloud security groups."
6986
7002
  ).optional(),
6987
7003
  assignedLicenses: z.array(microsoft_graph_assignedLicense).describe(
6988
7004
  "The licenses that are assigned to the group. Requires $select to retrieve. Supports $filter (eq). Read-only."
@@ -14109,6 +14125,24 @@ To monitor future changes, call the delta API by using the @odata.deltaLink in t
14109
14125
  ],
14110
14126
  response: z.void()
14111
14127
  },
14128
+ {
14129
+ method: "patch",
14130
+ path: "/teams/:teamId/channels/:channelId/messages/:chatMessageId",
14131
+ alias: "update-channel-message",
14132
+ description: `Update a chatMessage object.
14133
+ Except for the policyViolation property, all properties of a chatMessage can be updated in delegated permissions scenarios.
14134
+ Only the policyViolation property of a chatMessage can be updated in application permissions scenarios. The update only works for chats where members are Microsoft Teams users. If one of the participants is using Skype, the operation fails. This method doesn't support federation. Only the user in the tenant who sent the message can perform data loss prevention (DLP) updates on the specified chat message.`,
14135
+ requestFormat: "json",
14136
+ parameters: [
14137
+ {
14138
+ name: "body",
14139
+ description: `New navigation property values`,
14140
+ type: "Body",
14141
+ schema: microsoft_graph_chatMessage
14142
+ }
14143
+ ],
14144
+ response: z.void()
14145
+ },
14112
14146
  {
14113
14147
  method: "get",
14114
14148
  path: "/teams/:teamId/channels/:channelId/messages/:chatMessageId/hostedContents",
@@ -14225,6 +14259,22 @@ To monitor future changes, call the delta API by using the @odata.deltaLink in t
14225
14259
  ],
14226
14260
  response: z.void()
14227
14261
  },
14262
+ {
14263
+ method: "patch",
14264
+ path: "/teams/:teamId/channels/:channelId/messages/:chatMessageId/replies/:chatMessageId1",
14265
+ alias: "update-channel-message-reply",
14266
+ description: `Update the navigation property replies in teams`,
14267
+ requestFormat: "json",
14268
+ parameters: [
14269
+ {
14270
+ name: "body",
14271
+ description: `New navigation property values`,
14272
+ type: "Body",
14273
+ schema: microsoft_graph_chatMessage
14274
+ }
14275
+ ],
14276
+ response: z.void()
14277
+ },
14228
14278
  {
14229
14279
  method: "post",
14230
14280
  path: "/teams/:teamId/channels/:channelId/messages/:chatMessageId/setReaction",
@@ -133,11 +133,25 @@ class GraphClient {
133
133
  }
134
134
  return JSON.stringify(data, null, pretty ? 2 : void 0);
135
135
  }
136
+ /**
137
+ * Encode a value in the configured format (json/toon). The fetchAllPages merge
138
+ * uses this to encode the combined result once, after parsing pages as JSON (#560).
139
+ * Compact by default like JSON.stringify, so JSON-mode output is byte-identical.
140
+ */
141
+ serialize(data, pretty = false) {
142
+ return this.serializeData(data, this.outputFormat, pretty);
143
+ }
136
144
  async graphRequest(endpoint, options = {}) {
137
145
  try {
138
146
  logger.info(`Calling ${endpoint} with options: ${JSON.stringify(options)}`);
139
147
  const result = await this.makeRequest(endpoint, options);
140
- return this.formatJsonResponse(result, options.rawResponse, options.excludeResponse);
148
+ const outputFormat = options.forceJsonOutput ? "json" : this.outputFormat;
149
+ return this.formatJsonResponse(
150
+ result,
151
+ options.rawResponse,
152
+ options.excludeResponse,
153
+ outputFormat
154
+ );
141
155
  } catch (error) {
142
156
  logger.error(`Error in Graph API request: ${error}`);
143
157
  return {
@@ -146,10 +160,10 @@ class GraphClient {
146
160
  };
147
161
  }
148
162
  }
149
- formatJsonResponse(data, rawResponse = false, excludeResponse = false) {
163
+ formatJsonResponse(data, rawResponse = false, excludeResponse = false, outputFormat = this.outputFormat) {
150
164
  if (excludeResponse) {
151
165
  return {
152
- content: [{ type: "text", text: this.serializeData({ success: true }, this.outputFormat) }]
166
+ content: [{ type: "text", text: this.serializeData({ success: true }, outputFormat) }]
153
167
  };
154
168
  }
155
169
  if (data && typeof data === "object" && "_headers" in data) {
@@ -163,17 +177,13 @@ class GraphClient {
163
177
  }
164
178
  if (rawResponse) {
165
179
  return {
166
- content: [
167
- { type: "text", text: this.serializeData(responseData.data, this.outputFormat) }
168
- ],
180
+ content: [{ type: "text", text: this.serializeData(responseData.data, outputFormat) }],
169
181
  _meta: meta
170
182
  };
171
183
  }
172
184
  if (responseData.data === null || responseData.data === void 0) {
173
185
  return {
174
- content: [
175
- { type: "text", text: this.serializeData({ success: true }, this.outputFormat) }
176
- ],
186
+ content: [{ type: "text", text: this.serializeData({ success: true }, outputFormat) }],
177
187
  _meta: meta
178
188
  };
179
189
  }
@@ -191,19 +201,19 @@ class GraphClient {
191
201
  removeODataProps2(responseData.data);
192
202
  return {
193
203
  content: [
194
- { type: "text", text: this.serializeData(responseData.data, this.outputFormat, true) }
204
+ { type: "text", text: this.serializeData(responseData.data, outputFormat, true) }
195
205
  ],
196
206
  _meta: meta
197
207
  };
198
208
  }
199
209
  if (rawResponse) {
200
210
  return {
201
- content: [{ type: "text", text: this.serializeData(data, this.outputFormat) }]
211
+ content: [{ type: "text", text: this.serializeData(data, outputFormat) }]
202
212
  };
203
213
  }
204
214
  if (data === null || data === void 0) {
205
215
  return {
206
- content: [{ type: "text", text: this.serializeData({ success: true }, this.outputFormat) }]
216
+ content: [{ type: "text", text: this.serializeData({ success: true }, outputFormat) }]
207
217
  };
208
218
  }
209
219
  const removeODataProps = (obj) => {
@@ -219,7 +229,7 @@ class GraphClient {
219
229
  };
220
230
  removeODataProps(data);
221
231
  return {
222
- content: [{ type: "text", text: this.serializeData(data, this.outputFormat, true) }]
232
+ content: [{ type: "text", text: this.serializeData(data, outputFormat, true) }]
223
233
  };
224
234
  }
225
235
  }
@@ -328,7 +328,10 @@ const UTILITY_TOOLS = [
328
328
  accountAccessToken = await authManager.getTokenForAccount(accountParam);
329
329
  }
330
330
  const response = await graphClient.graphRequest(itemPath, {
331
- accessToken: accountAccessToken
331
+ accessToken: accountAccessToken,
332
+ // We JSON.parse the metadata below, so force JSON - under --toon it'd be
333
+ // TOON and the parse would fail, masking a real item as "no download url".
334
+ forceJsonOutput: true
332
335
  });
333
336
  if (response?.isError) {
334
337
  return response;
@@ -612,7 +615,6 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
612
615
  logger.info(
613
616
  `Making graph request to ${path2} with options: ${JSON.stringify(safeOptions)}${_redacted ? " [accessToken=REDACTED]" : ""}`
614
617
  );
615
- let response = await graphClient.graphRequest(path2, options);
616
618
  const fetchAllPages = params.fetchAllPages === true;
617
619
  const paginationEnabled = paginationAllowed();
618
620
  if (fetchAllPages && !paginationEnabled) {
@@ -620,58 +622,69 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
620
622
  "fetchAllPages requested but MS365_MCP_ALLOW_PAGINATION is disabled; returning first page only"
621
623
  );
622
624
  }
623
- if (fetchAllPages && paginationEnabled && response?.content?.[0]?.text) {
625
+ const mergePages = fetchAllPages && paginationEnabled;
626
+ if (mergePages) {
627
+ options.forceJsonOutput = true;
628
+ }
629
+ let response = await graphClient.graphRequest(path2, options);
630
+ if (mergePages && response?.content?.[0]?.text) {
631
+ let combinedResponse;
624
632
  try {
625
- let combinedResponse = JSON.parse(response.content[0].text);
626
- let allItems = combinedResponse.value || [];
627
- let nextLink = combinedResponse["@odata.nextLink"];
628
- let pageCount = 1;
629
- const maxPages = positiveIntFromEnv("MS365_MCP_MAX_PAGES", DEFAULT_MAX_PAGES);
630
- const maxItems = positiveIntFromEnv("MS365_MCP_MAX_ITEMS", DEFAULT_MAX_ITEMS);
631
- let deltaLink = combinedResponse["@odata.deltaLink"];
632
- while (nextLink && pageCount < maxPages && allItems.length < maxItems) {
633
- logger.info(`Fetching page ${pageCount + 1} from: ${nextLink}`);
634
- const url = new URL(nextLink);
635
- const nextPath = url.pathname.replace(/^\/(v1\.0|beta)/, "") + url.search;
636
- const nextOptions = { ...options };
637
- const nextResponse = await graphClient.graphRequest(nextPath, nextOptions);
638
- if (nextResponse?.content?.[0]?.text) {
639
- const nextJsonResponse = JSON.parse(nextResponse.content[0].text);
640
- if (nextJsonResponse.value && Array.isArray(nextJsonResponse.value)) {
641
- allItems = allItems.concat(nextJsonResponse.value);
642
- }
643
- nextLink = nextJsonResponse["@odata.nextLink"];
644
- if (nextJsonResponse["@odata.deltaLink"]) {
645
- deltaLink = nextJsonResponse["@odata.deltaLink"];
633
+ combinedResponse = JSON.parse(response.content[0].text);
634
+ const firstValue = combinedResponse.value;
635
+ if (Array.isArray(firstValue)) {
636
+ let allItems = firstValue;
637
+ let nextLink = combinedResponse["@odata.nextLink"];
638
+ let pageCount = 1;
639
+ const maxPages = positiveIntFromEnv("MS365_MCP_MAX_PAGES", DEFAULT_MAX_PAGES);
640
+ const maxItems = positiveIntFromEnv("MS365_MCP_MAX_ITEMS", DEFAULT_MAX_ITEMS);
641
+ let deltaLink = combinedResponse["@odata.deltaLink"];
642
+ while (nextLink && pageCount < maxPages && allItems.length < maxItems) {
643
+ logger.info(`Fetching page ${pageCount + 1} from: ${nextLink}`);
644
+ const url = new URL(nextLink);
645
+ const nextPath = url.pathname.replace(/^\/(v1\.0|beta)/, "") + url.search;
646
+ const nextOptions = { ...options };
647
+ const nextResponse = await graphClient.graphRequest(nextPath, nextOptions);
648
+ if (nextResponse?.content?.[0]?.text) {
649
+ const nextJsonResponse = JSON.parse(nextResponse.content[0].text);
650
+ if (Array.isArray(nextJsonResponse.value)) {
651
+ allItems = allItems.concat(nextJsonResponse.value);
652
+ }
653
+ nextLink = nextJsonResponse["@odata.nextLink"];
654
+ if (nextJsonResponse["@odata.deltaLink"]) {
655
+ deltaLink = nextJsonResponse["@odata.deltaLink"];
656
+ }
657
+ pageCount++;
658
+ } else {
659
+ break;
646
660
  }
647
- pageCount++;
648
- } else {
649
- break;
650
661
  }
651
- }
652
- if (pageCount >= maxPages) {
653
- logger.warn(`Reached maximum page limit (${maxPages}) for pagination`);
654
- }
655
- if (allItems.length >= maxItems) {
656
- logger.warn(
657
- `Reached maximum item limit (${maxItems}) for pagination \u2014 truncated at ${allItems.length} items`
662
+ if (pageCount >= maxPages) {
663
+ logger.warn(`Reached maximum page limit (${maxPages}) for pagination`);
664
+ }
665
+ if (allItems.length >= maxItems) {
666
+ logger.warn(
667
+ `Reached maximum item limit (${maxItems}) for pagination \u2014 truncated at ${allItems.length} items`
668
+ );
669
+ }
670
+ combinedResponse.value = allItems;
671
+ if (combinedResponse["@odata.count"]) {
672
+ combinedResponse["@odata.count"] = allItems.length;
673
+ }
674
+ delete combinedResponse["@odata.nextLink"];
675
+ if (deltaLink) {
676
+ combinedResponse["@odata.deltaLink"] = deltaLink;
677
+ }
678
+ logger.info(
679
+ `Pagination complete: collected ${allItems.length} items across ${pageCount} pages`
658
680
  );
659
681
  }
660
- combinedResponse.value = allItems;
661
- if (combinedResponse["@odata.count"]) {
662
- combinedResponse["@odata.count"] = allItems.length;
663
- }
664
- delete combinedResponse["@odata.nextLink"];
665
- if (deltaLink) {
666
- combinedResponse["@odata.deltaLink"] = deltaLink;
667
- }
668
- response.content[0].text = JSON.stringify(combinedResponse);
669
- logger.info(
670
- `Pagination complete: collected ${allItems.length} items across ${pageCount} pages`
671
- );
672
682
  } catch (e) {
673
683
  logger.error(`Error during pagination: ${e}`);
674
684
  }
685
+ if (combinedResponse !== void 0) {
686
+ response.content[0].text = graphClient.serialize(combinedResponse);
687
+ }
675
688
  }
676
689
  if (response?.content?.[0]?.text) {
677
690
  const responseText = response.content[0].text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.129.0",
3
+ "version": "0.130.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",
@@ -1393,6 +1393,13 @@
1393
1393
  "presets": ["teams", "work"],
1394
1394
  "workScopes": ["ChatMessage.Read"]
1395
1395
  },
1396
+ {
1397
+ "pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}",
1398
+ "method": "patch",
1399
+ "toolName": "update-chat-message",
1400
+ "presets": ["teams", "work"],
1401
+ "workScopes": ["Chat.ReadWrite"]
1402
+ },
1396
1403
  {
1397
1404
  "pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/hostedContents",
1398
1405
  "method": "get",
@@ -1472,6 +1479,13 @@
1472
1479
  "presets": ["teams", "work"],
1473
1480
  "workScopes": ["ChannelMessage.Read.All"]
1474
1481
  },
1482
+ {
1483
+ "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}",
1484
+ "method": "patch",
1485
+ "toolName": "update-channel-message",
1486
+ "presets": ["teams", "work"],
1487
+ "workScopes": ["ChannelMessage.ReadWrite"]
1488
+ },
1475
1489
  {
1476
1490
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/hostedContents",
1477
1491
  "method": "get",
@@ -1503,6 +1517,13 @@
1503
1517
  "presets": ["teams", "work"],
1504
1518
  "workScopes": ["ChannelMessage.Read.All"]
1505
1519
  },
1520
+ {
1521
+ "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies/{chatMessage-id1}",
1522
+ "method": "patch",
1523
+ "toolName": "update-channel-message-reply",
1524
+ "presets": ["teams", "work"],
1525
+ "workScopes": ["ChannelMessage.ReadWrite"]
1526
+ },
1506
1527
  {
1507
1528
  "pathPattern": "/teams/{team-id}/members",
1508
1529
  "method": "post",