@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 +3 -0
- package/dist/__tests__/graph-tools.test.js +71 -0
- package/dist/graph-tools.js +26 -3
- package/package.json +1 -1
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 () => {
|
package/dist/graph-tools.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
369
|
-
const maxItems =
|
|
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.
|
|
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",
|