@softeria/ms-365-mcp-server 0.161.0 → 0.161.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.
|
@@ -458,9 +458,10 @@ describe("graph-tools", () => {
|
|
|
458
458
|
const server = createMockServer();
|
|
459
459
|
const { registerGraphTools } = await loadModule();
|
|
460
460
|
registerGraphTools(server, graphClient);
|
|
461
|
-
await server.tools.get("test-tool").handler({ fetchAllPages: true });
|
|
461
|
+
const result = await server.tools.get("test-tool").handler({ fetchAllPages: true });
|
|
462
462
|
const [payload] = auditLogMock.mock.calls[0];
|
|
463
463
|
expect(payload.result_count).toBe(2);
|
|
464
|
+
expect(JSON.parse(result.content[0].text)["@odata.nextLink"]).toBeUndefined();
|
|
464
465
|
expect(payload.result_has_more).toBe(true);
|
|
465
466
|
expect(payload.response_bytes).toBe(100);
|
|
466
467
|
} finally {
|
|
@@ -471,6 +472,64 @@ describe("graph-tools", () => {
|
|
|
471
472
|
}
|
|
472
473
|
}
|
|
473
474
|
});
|
|
475
|
+
it("reports result_has_more when a page is not a collection", async () => {
|
|
476
|
+
mockEndpoints.push(makeEndpoint());
|
|
477
|
+
mockEndpointsJson = [makeConfig()];
|
|
478
|
+
const pageThreeLink = "https://graph.microsoft.com/v1.0/me/messages?$skip=4";
|
|
479
|
+
const graphClient = createMockGraphClient([
|
|
480
|
+
{
|
|
481
|
+
content: [
|
|
482
|
+
{
|
|
483
|
+
type: "text",
|
|
484
|
+
text: JSON.stringify({
|
|
485
|
+
value: [{ id: "1" }, { id: "2" }],
|
|
486
|
+
"@odata.nextLink": "https://graph.microsoft.com/v1.0/me/messages?$skip=2"
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
],
|
|
490
|
+
_meta: { http_status: 200, result_count: 2, result_has_more: true, response_bytes: 1e3 }
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
content: [
|
|
494
|
+
{
|
|
495
|
+
type: "text",
|
|
496
|
+
text: JSON.stringify({
|
|
497
|
+
value: [{ id: "3" }, { id: "4" }],
|
|
498
|
+
"@odata.nextLink": pageThreeLink
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
],
|
|
502
|
+
_meta: { http_status: 200, result_count: 2, result_has_more: true, response_bytes: 900 }
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
content: [
|
|
506
|
+
{
|
|
507
|
+
type: "text",
|
|
508
|
+
text: JSON.stringify({
|
|
509
|
+
message: "OK!",
|
|
510
|
+
contentType: "application/json",
|
|
511
|
+
encoding: "base64",
|
|
512
|
+
contentLength: 9,
|
|
513
|
+
contentBytes: "eyJ2YWx1"
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
],
|
|
517
|
+
_meta: { http_status: 200, response_bytes: 700 }
|
|
518
|
+
}
|
|
519
|
+
]);
|
|
520
|
+
const server = createMockServer();
|
|
521
|
+
const { registerGraphTools } = await loadModule();
|
|
522
|
+
registerGraphTools(server, graphClient);
|
|
523
|
+
const result = await server.tools.get("test-tool").handler({ fetchAllPages: true });
|
|
524
|
+
expect(graphClient.graphRequest).toHaveBeenCalledTimes(3);
|
|
525
|
+
const body = JSON.parse(result.content[0].text);
|
|
526
|
+
expect(body.value.map((item) => item.id)).toEqual(["1", "2", "3", "4"]);
|
|
527
|
+
expect(body["@odata.nextLink"]).toBe(pageThreeLink);
|
|
528
|
+
const [payload] = auditLogMock.mock.calls[0];
|
|
529
|
+
expect(payload.result_count).toBe(4);
|
|
530
|
+
expect(payload.result_has_more).toBe(true);
|
|
531
|
+
expect(payload.response_bytes).toBe(1900);
|
|
532
|
+
});
|
|
474
533
|
it("omits the volume fields when the client supplied none", async () => {
|
|
475
534
|
const endpoint = makeEndpoint({ method: "get", path: "/me", alias: "get-current-user" });
|
|
476
535
|
const config = makeConfig({
|
package/dist/endpoints.json
CHANGED
|
@@ -2324,6 +2324,7 @@
|
|
|
2324
2324
|
"pathPattern": "/groups/{group-id}/threads/{conversationThread-id}/reply",
|
|
2325
2325
|
"method": "post",
|
|
2326
2326
|
"toolName": "reply-to-group-thread",
|
|
2327
|
+
"descriptionOverride": "Reply to a conversation thread in a Microsoft 365 group, adding a new post to the thread. Use list-group-threads to obtain the thread ID.",
|
|
2327
2328
|
"presets": ["work"],
|
|
2328
2329
|
"workScopes": ["Group.ReadWrite.All"],
|
|
2329
2330
|
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
|
package/dist/graph-tools.js
CHANGED
|
@@ -1726,6 +1726,7 @@ async function executeGraphTool(tool, config, graphClient, params, authManager,
|
|
|
1726
1726
|
const maxPages = positiveIntFromEnv("MS365_MCP_MAX_PAGES", DEFAULT_MAX_PAGES);
|
|
1727
1727
|
const maxItems = positiveIntFromEnv("MS365_MCP_MAX_ITEMS", DEFAULT_MAX_ITEMS);
|
|
1728
1728
|
let deltaLink = combinedResponse["@odata.deltaLink"];
|
|
1729
|
+
let unmergeablePage = false;
|
|
1729
1730
|
while (nextLink && pageCount < maxPages && allItems.length < maxItems) {
|
|
1730
1731
|
logger.info(`Fetching page ${pageCount + 1} from: ${nextLink}`);
|
|
1731
1732
|
const url = new URL(nextLink);
|
|
@@ -1739,9 +1740,14 @@ async function executeGraphTool(tool, config, graphClient, params, authManager,
|
|
|
1739
1740
|
}
|
|
1740
1741
|
if (nextResponse?.content?.[0]?.text) {
|
|
1741
1742
|
const nextJsonResponse = JSON.parse(nextResponse.content[0].text);
|
|
1742
|
-
if (Array.isArray(nextJsonResponse.value)) {
|
|
1743
|
-
|
|
1743
|
+
if (!Array.isArray(nextJsonResponse.value)) {
|
|
1744
|
+
logger.warn(
|
|
1745
|
+
`Pagination stopped at page ${pageCount + 1}: response was not a collection`
|
|
1746
|
+
);
|
|
1747
|
+
unmergeablePage = true;
|
|
1748
|
+
break;
|
|
1744
1749
|
}
|
|
1750
|
+
allItems = allItems.concat(nextJsonResponse.value);
|
|
1745
1751
|
nextLink = nextJsonResponse["@odata.nextLink"];
|
|
1746
1752
|
if (typeof totalResponseBytes === "number" && typeof nextResponse._meta?.response_bytes === "number") {
|
|
1747
1753
|
totalResponseBytes += nextResponse._meta.response_bytes;
|
|
@@ -1751,6 +1757,7 @@ async function executeGraphTool(tool, config, graphClient, params, authManager,
|
|
|
1751
1757
|
}
|
|
1752
1758
|
pageCount++;
|
|
1753
1759
|
} else {
|
|
1760
|
+
unmergeablePage = true;
|
|
1754
1761
|
break;
|
|
1755
1762
|
}
|
|
1756
1763
|
}
|
|
@@ -1767,7 +1774,11 @@ async function executeGraphTool(tool, config, graphClient, params, authManager,
|
|
|
1767
1774
|
if (combinedResponse["@odata.count"]) {
|
|
1768
1775
|
combinedResponse["@odata.count"] = allItems.length;
|
|
1769
1776
|
}
|
|
1770
|
-
|
|
1777
|
+
if (unmergeablePage && nextLink) {
|
|
1778
|
+
combinedResponse["@odata.nextLink"] = nextLink;
|
|
1779
|
+
} else {
|
|
1780
|
+
delete combinedResponse["@odata.nextLink"];
|
|
1781
|
+
}
|
|
1771
1782
|
response._meta = {
|
|
1772
1783
|
...response._meta,
|
|
1773
1784
|
result_count: allItems.length,
|
|
@@ -1778,7 +1789,7 @@ async function executeGraphTool(tool, config, graphClient, params, authManager,
|
|
|
1778
1789
|
combinedResponse["@odata.deltaLink"] = deltaLink;
|
|
1779
1790
|
}
|
|
1780
1791
|
logger.info(
|
|
1781
|
-
`Pagination complete: collected ${allItems.length} items across ${pageCount} pages`
|
|
1792
|
+
`Pagination ${nextLink ? "incomplete" : "complete"}: collected ${allItems.length} items across ${pageCount} pages`
|
|
1782
1793
|
);
|
|
1783
1794
|
}
|
|
1784
1795
|
}
|
|
@@ -31,7 +31,7 @@ function isFetchAllPagesApplicable(tool) {
|
|
|
31
31
|
const FILTER_PARAM_DESCRIPTION = "OData filter expression. Add $count=true for advanced filters (flag/flagStatus, contains()). Cannot combine with $search.";
|
|
32
32
|
const SEARCH_PARAM_DESCRIPTION = "KQL search query in one pair of double quotes; directory (users/groups) instead quotes each clause with no outer pair. Cannot combine with $filter.";
|
|
33
33
|
const SELECT_PARAM_DESCRIPTION = "Comma-separated fields to return, e.g. id,subject,from,receivedDateTime";
|
|
34
|
-
const EXPAND_PARAM_DESCRIPTION = 'Navigation properties to inline, e.g. attachments on a message or event. Only navigation properties can be expanded: expanding a non-navigation property such as a message body fails with "Parsing OData Select and Expand failed"
|
|
34
|
+
const EXPAND_PARAM_DESCRIPTION = 'Navigation properties to inline, e.g. attachments on a message or event. Only navigation properties can be expanded: expanding a non-navigation property such as a message body fails with "Parsing OData Select and Expand failed". Request ordinary fields with $select instead. Some APIs accept only one relationship per request, so prefer a single value. Some relationships support nested selection, e.g. children($select=id,name).';
|
|
35
35
|
const ORDERBY_PARAM_DESCRIPTION = "Sort expression, e.g. receivedDateTime desc";
|
|
36
36
|
const TOP_PARAM_DESCRIPTION = "Page size (Graph $top). Start small (e.g. 5\u201315) so responses fit the model context; raise only if needed. Use $select to return fewer fields per item. For more rows, use @odata.nextLink from the response instead of a very large $top.";
|
|
37
37
|
const SKIP_PARAM_DESCRIPTION = "Items to skip for pagination. Not supported with $search.";
|
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.161.
|
|
4
|
+
"version": "0.161.1",
|
|
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",
|
package/src/endpoints.json
CHANGED
|
@@ -2324,6 +2324,7 @@
|
|
|
2324
2324
|
"pathPattern": "/groups/{group-id}/threads/{conversationThread-id}/reply",
|
|
2325
2325
|
"method": "post",
|
|
2326
2326
|
"toolName": "reply-to-group-thread",
|
|
2327
|
+
"descriptionOverride": "Reply to a conversation thread in a Microsoft 365 group, adding a new post to the thread. Use list-group-threads to obtain the thread ID.",
|
|
2327
2328
|
"presets": ["work"],
|
|
2328
2329
|
"workScopes": ["Group.ReadWrite.All"],
|
|
2329
2330
|
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
|