@softeria/ms-365-mcp-server 0.115.0 → 0.116.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -573,6 +573,9 @@ Environment variables:
573
573
  - `MS365_MCP_FORCE_WORK_SCOPES=true|1`: Backwards compatibility for MS365_MCP_ORG_MODE
574
574
  - `MS365_MCP_OUTPUT_FORMAT=toon`: Enable TOON output format (alternative to --toon flag)
575
575
  - `MS365_MCP_MAX_TOP=<n>`: Hard cap for Graph `$top` / `top` on list requests (positive integer). When the model passes a larger value, the server clamps it to `n` so responses stay smaller. Example: `MS365_MCP_MAX_TOP=15`
576
+ - `MS365_MCP_MAX_PAGES=<n>`: Maximum number of pages followed when a tool is called with `fetchAllPages: true` (positive integer, default `100`). Bounds memory and latency for large result sets.
577
+ - `MS365_MCP_MAX_ITEMS=<n>`: Maximum number of items accumulated when `fetchAllPages: true` (positive integer, default `10000`). Pagination stops and the response is truncated once this many items are collected.
578
+ - `MS365_MCP_ALLOW_PAGINATION=0|false|no`: Disable multi-page following entirely. When set, `fetchAllPages: true` returns only the first page (default: pagination enabled).
576
579
  - `MS365_MCP_BODY_FORMAT=html`: Return email bodies as HTML instead of plain text (default: text)
577
580
  - `MS365_MCP_CLOUD_TYPE=global|china`: Microsoft cloud environment (alternative to --cloud flag)
578
581
  - `LOG_LEVEL`: Set logging level (default: 'info')
@@ -183,6 +183,77 @@ describe("graph-tools", () => {
183
183
  await tool.handler({ fetchAllPages: true });
184
184
  expect(graphClient.graphRequest).toHaveBeenCalledTimes(100);
185
185
  });
186
+ describe("pagination env caps", () => {
187
+ const prev = {
188
+ pages: process.env.MS365_MCP_MAX_PAGES,
189
+ items: process.env.MS365_MCP_MAX_ITEMS,
190
+ allow: process.env.MS365_MCP_ALLOW_PAGINATION
191
+ };
192
+ afterEach(() => {
193
+ const restore = (name, value) => value === void 0 ? delete process.env[name] : process.env[name] = value;
194
+ restore("MS365_MCP_MAX_PAGES", prev.pages);
195
+ restore("MS365_MCP_MAX_ITEMS", prev.items);
196
+ restore("MS365_MCP_ALLOW_PAGINATION", prev.allow);
197
+ });
198
+ const paginatingResponses = (count) => Array.from({ length: count }, (_, i) => ({
199
+ content: [
200
+ {
201
+ type: "text",
202
+ text: JSON.stringify({
203
+ value: [{ id: `item-${i}` }],
204
+ "@odata.nextLink": "https://graph.microsoft.com/v1.0/me/messages?$skip=" + (i + 1)
205
+ })
206
+ }
207
+ ]
208
+ }));
209
+ it("should honor MS365_MCP_MAX_PAGES below the default", async () => {
210
+ process.env.MS365_MCP_MAX_PAGES = "2";
211
+ mockEndpoints.push(makeEndpoint());
212
+ mockEndpointsJson = [makeConfig()];
213
+ const graphClient = createMockGraphClient(paginatingResponses(5));
214
+ const server = createMockServer();
215
+ const { registerGraphTools } = await loadModule();
216
+ registerGraphTools(server, graphClient);
217
+ await server.tools.get("test-tool").handler({ fetchAllPages: true });
218
+ expect(graphClient.graphRequest).toHaveBeenCalledTimes(2);
219
+ });
220
+ it("should honor MS365_MCP_MAX_ITEMS below the default", async () => {
221
+ process.env.MS365_MCP_MAX_ITEMS = "2";
222
+ mockEndpoints.push(makeEndpoint());
223
+ mockEndpointsJson = [makeConfig()];
224
+ const graphClient = createMockGraphClient([
225
+ {
226
+ content: [
227
+ {
228
+ type: "text",
229
+ text: JSON.stringify({
230
+ value: [{ id: "1" }, { id: "2" }],
231
+ "@odata.nextLink": "https://graph.microsoft.com/v1.0/me/messages?$skip=2"
232
+ })
233
+ }
234
+ ]
235
+ },
236
+ ...paginatingResponses(3)
237
+ ]);
238
+ const server = createMockServer();
239
+ const { registerGraphTools } = await loadModule();
240
+ registerGraphTools(server, graphClient);
241
+ const result = await server.tools.get("test-tool").handler({ fetchAllPages: true });
242
+ expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
243
+ expect(JSON.parse(result.content[0].text).value).toHaveLength(2);
244
+ });
245
+ it("should not follow nextLink when MS365_MCP_ALLOW_PAGINATION is disabled", async () => {
246
+ process.env.MS365_MCP_ALLOW_PAGINATION = "0";
247
+ mockEndpoints.push(makeEndpoint());
248
+ mockEndpointsJson = [makeConfig()];
249
+ const graphClient = createMockGraphClient(paginatingResponses(5));
250
+ const server = createMockServer();
251
+ const { registerGraphTools } = await loadModule();
252
+ registerGraphTools(server, graphClient);
253
+ await server.tools.get("test-tool").handler({ fetchAllPages: true });
254
+ expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
255
+ });
256
+ });
186
257
  });
187
258
  describe("parameter describe() overrides", () => {
188
259
  it("should apply custom descriptions to OData parameters", async () => {
@@ -41,6 +41,23 @@ function clampTopQueryParam(queryParams) {
41
41
  logger.info(`Clamping $top from ${requested} to ${cap} (MS365_MCP_MAX_TOP)`);
42
42
  queryParams["$top"] = String(cap);
43
43
  }
44
+ const DEFAULT_MAX_PAGES = 100;
45
+ const DEFAULT_MAX_ITEMS = 1e4;
46
+ function positiveIntFromEnv(name, defaultValue) {
47
+ const raw = process.env[name];
48
+ if (raw === void 0 || raw === "") return defaultValue;
49
+ const n = Number.parseInt(raw, 10);
50
+ if (!Number.isFinite(n) || n < 1) {
51
+ logger.warn(`Ignoring invalid ${name}=${JSON.stringify(raw)} (use a positive integer)`);
52
+ return defaultValue;
53
+ }
54
+ return n;
55
+ }
56
+ function paginationAllowed() {
57
+ const raw = process.env.MS365_MCP_ALLOW_PAGINATION;
58
+ if (raw === void 0 || raw === "") return true;
59
+ return !/^(0|false|no)$/i.test(raw.trim());
60
+ }
44
61
  function formatDisabledToolsForLog(disabledTools) {
45
62
  const shown = disabledTools.slice(0, 20).map((tool) => `${tool.toolName} (missing: ${tool.missingScopes.join(", ")})`);
46
63
  const suffix = disabledTools.length > shown.length ? `, ... +${disabledTools.length - shown.length} more` : "";
@@ -359,14 +376,20 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
359
376
  );
360
377
  let response = await graphClient.graphRequest(path2, options);
361
378
  const fetchAllPages = params.fetchAllPages === true;
362
- if (fetchAllPages && response?.content?.[0]?.text) {
379
+ const paginationEnabled = paginationAllowed();
380
+ if (fetchAllPages && !paginationEnabled) {
381
+ logger.info(
382
+ "fetchAllPages requested but MS365_MCP_ALLOW_PAGINATION is disabled; returning first page only"
383
+ );
384
+ }
385
+ if (fetchAllPages && paginationEnabled && response?.content?.[0]?.text) {
363
386
  try {
364
387
  let combinedResponse = JSON.parse(response.content[0].text);
365
388
  let allItems = combinedResponse.value || [];
366
389
  let nextLink = combinedResponse["@odata.nextLink"];
367
390
  let pageCount = 1;
368
- const maxPages = 100;
369
- const maxItems = 1e4;
391
+ const maxPages = positiveIntFromEnv("MS365_MCP_MAX_PAGES", DEFAULT_MAX_PAGES);
392
+ const maxItems = positiveIntFromEnv("MS365_MCP_MAX_ITEMS", DEFAULT_MAX_ITEMS);
370
393
  while (nextLink && pageCount < maxPages && allItems.length < maxItems) {
371
394
  logger.info(`Fetching page ${pageCount + 1} from: ${nextLink}`);
372
395
  const url = new URL(nextLink);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.115.0",
3
+ "version": "0.116.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",