@softeria/ms-365-mcp-server 0.149.0 → 0.150.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/dist/__tests__/graph-tools.test.js +191 -0
- package/dist/endpoints.json +4 -4
- package/dist/graph-client.js +5 -4
- package/dist/graph-tools.js +37 -2
- package/dist/lib/select-projection.js +82 -0
- package/package.json +1 -1
- package/src/endpoints.json +4 -4
|
@@ -2822,6 +2822,197 @@ describe("graph-tools", () => {
|
|
|
2822
2822
|
expect(server.tools.get("destructive-tool").schema).toHaveProperty("confirm");
|
|
2823
2823
|
});
|
|
2824
2824
|
});
|
|
2825
|
+
describe("$select projection", () => {
|
|
2826
|
+
const untrimmed = {
|
|
2827
|
+
value: [
|
|
2828
|
+
{
|
|
2829
|
+
id: "MSo",
|
|
2830
|
+
subject: "Standup",
|
|
2831
|
+
joinWebUrl: "https://teams/x",
|
|
2832
|
+
joinInformation: { content: "<div>invite</div>" },
|
|
2833
|
+
joinMeetingIdSettings: { passcode: "123456" }
|
|
2834
|
+
}
|
|
2835
|
+
]
|
|
2836
|
+
};
|
|
2837
|
+
async function run(args, responses, outputFormat = "json") {
|
|
2838
|
+
mockEndpoints.push(makeEndpoint());
|
|
2839
|
+
mockEndpointsJson = [makeConfig()];
|
|
2840
|
+
const graphClient = createMockGraphClient(
|
|
2841
|
+
responses ?? [{ content: [{ type: "text", text: JSON.stringify(untrimmed) }] }],
|
|
2842
|
+
outputFormat
|
|
2843
|
+
);
|
|
2844
|
+
const server = createMockServer();
|
|
2845
|
+
const { registerGraphTools } = await loadModule();
|
|
2846
|
+
registerGraphTools(server, graphClient);
|
|
2847
|
+
const result = await server.tools.get("test-tool").handler(args);
|
|
2848
|
+
return { result, graphClient };
|
|
2849
|
+
}
|
|
2850
|
+
it("narrows the body to the requested fields when Graph ignored $select", async () => {
|
|
2851
|
+
const { result } = await run({ select: "id,subject,joinWebUrl" });
|
|
2852
|
+
expect(JSON.parse(result.content[0].text).value[0]).toEqual({
|
|
2853
|
+
id: "MSo",
|
|
2854
|
+
subject: "Standup",
|
|
2855
|
+
joinWebUrl: "https://teams/x"
|
|
2856
|
+
});
|
|
2857
|
+
});
|
|
2858
|
+
it("leaves the body untouched when no select was passed", async () => {
|
|
2859
|
+
const { result } = await run({});
|
|
2860
|
+
expect(JSON.parse(result.content[0].text)).toEqual(untrimmed);
|
|
2861
|
+
});
|
|
2862
|
+
it("forces JSON so a --toon client still gets a trimmed body", async () => {
|
|
2863
|
+
const { result, graphClient } = await run({ select: "id,subject" }, void 0, "toon");
|
|
2864
|
+
expect(graphClient.graphRequest).toHaveBeenCalledWith(
|
|
2865
|
+
expect.any(String),
|
|
2866
|
+
expect.objectContaining({ forceJsonOutput: true })
|
|
2867
|
+
);
|
|
2868
|
+
expect(result.content[0].text.startsWith("TOON:")).toBe(true);
|
|
2869
|
+
expect(result.content[0].text).not.toContain("joinInformation");
|
|
2870
|
+
});
|
|
2871
|
+
it("still projects when --toon and fetchAllPages are combined", async () => {
|
|
2872
|
+
const { result } = await run(
|
|
2873
|
+
{ select: "id,subject", fetchAllPages: true },
|
|
2874
|
+
[
|
|
2875
|
+
{
|
|
2876
|
+
content: [
|
|
2877
|
+
{
|
|
2878
|
+
type: "text",
|
|
2879
|
+
text: JSON.stringify({
|
|
2880
|
+
value: [{ id: "1", subject: "a", joinInformation: { content: "huge" } }],
|
|
2881
|
+
"@odata.nextLink": "https://graph.microsoft.com/v1.0/me/messages?$skip=1"
|
|
2882
|
+
})
|
|
2883
|
+
}
|
|
2884
|
+
]
|
|
2885
|
+
},
|
|
2886
|
+
{
|
|
2887
|
+
content: [
|
|
2888
|
+
{
|
|
2889
|
+
type: "text",
|
|
2890
|
+
text: JSON.stringify({
|
|
2891
|
+
value: [{ id: "2", subject: "b", joinInformation: { content: "huge" } }]
|
|
2892
|
+
})
|
|
2893
|
+
}
|
|
2894
|
+
]
|
|
2895
|
+
}
|
|
2896
|
+
],
|
|
2897
|
+
"toon"
|
|
2898
|
+
);
|
|
2899
|
+
expect(result.content[0].text.startsWith("TOON:")).toBe(true);
|
|
2900
|
+
expect(result.content[0].text).not.toContain("joinInformation");
|
|
2901
|
+
const merged = JSON.parse(result.content[0].text.slice("TOON:".length));
|
|
2902
|
+
expect(merged.value).toEqual([
|
|
2903
|
+
{ id: "1", subject: "a" },
|
|
2904
|
+
{ id: "2", subject: "b" }
|
|
2905
|
+
]);
|
|
2906
|
+
});
|
|
2907
|
+
it("does not project a raw-text transport envelope", async () => {
|
|
2908
|
+
const envelope = { message: "OK!", rawResponse: "WEBVTT\n\n00:00:01.000 --> 00:00:02.000" };
|
|
2909
|
+
const { result } = await run({ select: "id" }, [
|
|
2910
|
+
{ content: [{ type: "text", text: JSON.stringify(envelope) }] }
|
|
2911
|
+
]);
|
|
2912
|
+
expect(JSON.parse(result.content[0].text)).toEqual(envelope);
|
|
2913
|
+
});
|
|
2914
|
+
it("does not project a binary transport envelope", async () => {
|
|
2915
|
+
const envelope = {
|
|
2916
|
+
message: "OK!",
|
|
2917
|
+
contentType: "video/mp4",
|
|
2918
|
+
encoding: "base64",
|
|
2919
|
+
contentLength: 3,
|
|
2920
|
+
contentBytes: "AAA"
|
|
2921
|
+
};
|
|
2922
|
+
const { result } = await run({ select: "id" }, [
|
|
2923
|
+
{ content: [{ type: "text", text: JSON.stringify(envelope) }] }
|
|
2924
|
+
]);
|
|
2925
|
+
expect(JSON.parse(result.content[0].text)).toEqual(envelope);
|
|
2926
|
+
});
|
|
2927
|
+
it("keeps an $expand-ed navigation property that was not selected", async () => {
|
|
2928
|
+
const { result } = await run({ select: "subject", expand: "attachments" }, [
|
|
2929
|
+
{
|
|
2930
|
+
content: [
|
|
2931
|
+
{
|
|
2932
|
+
type: "text",
|
|
2933
|
+
text: JSON.stringify({
|
|
2934
|
+
id: "1",
|
|
2935
|
+
subject: "a",
|
|
2936
|
+
attachments: [{ id: "att1" }],
|
|
2937
|
+
bodyPreview: "huge"
|
|
2938
|
+
})
|
|
2939
|
+
}
|
|
2940
|
+
]
|
|
2941
|
+
}
|
|
2942
|
+
]);
|
|
2943
|
+
const body = JSON.parse(result.content[0].text);
|
|
2944
|
+
expect(body.attachments).toEqual([{ id: "att1" }]);
|
|
2945
|
+
expect(body).not.toHaveProperty("bodyPreview");
|
|
2946
|
+
});
|
|
2947
|
+
it("returns the body untrimmed when no requested field is present", async () => {
|
|
2948
|
+
const body = { id: "1", joinWebUrl: "u", joinInformation: { content: "huge" } };
|
|
2949
|
+
const { result } = await run({ select: "joinUrl" }, [
|
|
2950
|
+
{ content: [{ type: "text", text: JSON.stringify(body) }] }
|
|
2951
|
+
]);
|
|
2952
|
+
expect(JSON.parse(result.content[0].text)).toEqual(body);
|
|
2953
|
+
});
|
|
2954
|
+
it("keeps the _etag that includeHeaders adds to a single resource", async () => {
|
|
2955
|
+
const { result } = await run({ select: "subject", includeHeaders: true }, [
|
|
2956
|
+
{
|
|
2957
|
+
content: [
|
|
2958
|
+
{
|
|
2959
|
+
type: "text",
|
|
2960
|
+
text: JSON.stringify({ id: "1", subject: "a", _etag: 'W/"1"', bodyPreview: "huge" })
|
|
2961
|
+
}
|
|
2962
|
+
]
|
|
2963
|
+
}
|
|
2964
|
+
]);
|
|
2965
|
+
expect(JSON.parse(result.content[0].text)).toEqual({
|
|
2966
|
+
id: "1",
|
|
2967
|
+
subject: "a",
|
|
2968
|
+
_etag: 'W/"1"'
|
|
2969
|
+
});
|
|
2970
|
+
});
|
|
2971
|
+
it("does not arm projection when excludeResponse was requested", async () => {
|
|
2972
|
+
const { result, graphClient } = await run({ select: "id", excludeResponse: true }, [
|
|
2973
|
+
{ content: [{ type: "text", text: JSON.stringify({ success: true }) }] }
|
|
2974
|
+
]);
|
|
2975
|
+
expect(graphClient.graphRequest).toHaveBeenCalledWith(
|
|
2976
|
+
expect.any(String),
|
|
2977
|
+
expect.not.objectContaining({ forceJsonOutput: true })
|
|
2978
|
+
);
|
|
2979
|
+
expect(JSON.parse(result.content[0].text)).toEqual({ success: true });
|
|
2980
|
+
});
|
|
2981
|
+
it("does not project an error body on the merge path", async () => {
|
|
2982
|
+
const errorBody = { error: "Microsoft Graph API error: 403 Forbidden", code: "accessDenied" };
|
|
2983
|
+
const { result } = await run({ select: "error", fetchAllPages: true }, [
|
|
2984
|
+
{ content: [{ type: "text", text: JSON.stringify(errorBody) }], isError: true }
|
|
2985
|
+
]);
|
|
2986
|
+
expect(JSON.parse(result.content[0].text)).toEqual(errorBody);
|
|
2987
|
+
});
|
|
2988
|
+
it("leaves an error from a later page alone", async () => {
|
|
2989
|
+
const errorBody = { error: "Microsoft Graph API error: 503 Service Unavailable" };
|
|
2990
|
+
const { result } = await run(
|
|
2991
|
+
{ select: "error", fetchAllPages: true },
|
|
2992
|
+
[
|
|
2993
|
+
{
|
|
2994
|
+
content: [
|
|
2995
|
+
{
|
|
2996
|
+
type: "text",
|
|
2997
|
+
text: JSON.stringify({
|
|
2998
|
+
value: [{ id: "1", subject: "a" }],
|
|
2999
|
+
"@odata.nextLink": "https://graph.microsoft.com/v1.0/me/messages?$skip=1"
|
|
3000
|
+
})
|
|
3001
|
+
}
|
|
3002
|
+
]
|
|
3003
|
+
},
|
|
3004
|
+
{ content: [{ type: "text", text: JSON.stringify(errorBody) }], isError: true }
|
|
3005
|
+
],
|
|
3006
|
+
"toon"
|
|
3007
|
+
);
|
|
3008
|
+
expect(result.content[0].text.startsWith("TOON:")).toBe(false);
|
|
3009
|
+
expect(JSON.parse(result.content[0].text)).toEqual(errorBody);
|
|
3010
|
+
});
|
|
3011
|
+
it("returns the body untrimmed when the only real field is a typo alongside id", async () => {
|
|
3012
|
+
const { result } = await run({ select: "id,joinUrl" });
|
|
3013
|
+
expect(JSON.parse(result.content[0].text)).toEqual(untrimmed);
|
|
3014
|
+
});
|
|
3015
|
+
});
|
|
2825
3016
|
describe("isDestructiveOperation", () => {
|
|
2826
3017
|
it("returns true for POST, PATCH, PUT, DELETE", async () => {
|
|
2827
3018
|
const { isDestructiveOperation } = await loadModule();
|
package/dist/endpoints.json
CHANGED
|
@@ -510,7 +510,7 @@
|
|
|
510
510
|
"scopes": ["Calendars.Read"],
|
|
511
511
|
"supportsTimezone": true,
|
|
512
512
|
"supportsExpandExtendedProperties": true,
|
|
513
|
-
"llmTip": "Returns expanded recurring event instances (not just seriesMaster) within a date range for the default calendar. Requires startDateTime and endDateTime query parameters in ISO 8601 format (e.g., 2024-01-01T00:00:00Z). Use get-specific-calendar-view if you need a non-default calendar. To find Teams meetings, use $
|
|
513
|
+
"llmTip": "Returns expanded recurring event instances (not just seriesMaster) within a date range for the default calendar. Requires startDateTime and endDateTime query parameters in ISO 8601 format (e.g., 2024-01-01T00:00:00Z). Use get-specific-calendar-view if you need a non-default calendar. To find Teams meetings, use $select=subject,start,isOnlineMeeting,onlineMeetingProvider,onlineMeeting and keep the events with isOnlineMeeting true and onlineMeetingProvider teamsForBusiness; neither property is filterable (isOnlineMeeting returns 400 ErrorInvalidProperty). To search by subject, use $filter=contains(subject,'keyword'). Teams meetings expose their join link as onlineMeeting/joinUrl; the event resource has no joinWebUrl property. Pass that joinUrl to list-online-meetings as $filter=JoinWebUrl eq '{url}' to reach transcripts."
|
|
514
514
|
},
|
|
515
515
|
{
|
|
516
516
|
"pathPattern": "/me/calendars/{calendar-id}/calendarView",
|
|
@@ -521,7 +521,7 @@
|
|
|
521
521
|
"scopes": ["Calendars.Read"],
|
|
522
522
|
"supportsTimezone": true,
|
|
523
523
|
"supportsExpandExtendedProperties": true,
|
|
524
|
-
"llmTip": "Returns expanded recurring event instances (not just seriesMaster) within a date range for a specific calendar. Requires startDateTime and endDateTime query parameters in ISO 8601 format (e.g., 2024-01-01T00:00:00Z). Each instance includes seriesMasterId and type (occurrence/exception) fields for recurring event linkage. Use fetchAllPages=true to retrieve all results when there are many events. To find Teams meetings, use $
|
|
524
|
+
"llmTip": "Returns expanded recurring event instances (not just seriesMaster) within a date range for a specific calendar. Requires startDateTime and endDateTime query parameters in ISO 8601 format (e.g., 2024-01-01T00:00:00Z). Each instance includes seriesMasterId and type (occurrence/exception) fields for recurring event linkage. Use fetchAllPages=true to retrieve all results when there are many events. To find Teams meetings, use $select=subject,start,isOnlineMeeting,onlineMeetingProvider,onlineMeeting and keep the events with isOnlineMeeting true and onlineMeetingProvider teamsForBusiness; neither property is filterable (isOnlineMeeting returns 400 ErrorInvalidProperty). Teams meetings expose their join link as onlineMeeting/joinUrl; the event resource has no joinWebUrl property. Pass that joinUrl to list-online-meetings as $filter=JoinWebUrl eq '{url}' to reach transcripts."
|
|
525
525
|
},
|
|
526
526
|
{
|
|
527
527
|
"pathPattern": "/me/calendar/getSchedule",
|
|
@@ -2081,7 +2081,7 @@
|
|
|
2081
2081
|
"toolName": "list-online-meetings",
|
|
2082
2082
|
"presets": ["teams", "work"],
|
|
2083
2083
|
"workScopes": ["OnlineMeetings.Read"],
|
|
2084
|
-
"llmTip": "Look up a meeting with $filter=JoinWebUrl eq '{url}', using the joinUrl from an event's onlineMeeting property. This filter is required: the collection cannot be listed or paged, and $search/$orderby are not supported. Without it Graph returns 400 InvalidArgument ('One of the required parameters to lookup meeting by QueryOptions is null or empty'). Returns the meeting ID needed for transcript access."
|
|
2084
|
+
"llmTip": "Look up a meeting with $filter=JoinWebUrl eq '{url}', using the joinUrl from an event's onlineMeeting property. This filter is required: the collection cannot be listed or paged, and $search/$orderby are not supported. Without it Graph returns 400 InvalidArgument ('One of the required parameters to lookup meeting by QueryOptions is null or empty'). Returns the meeting ID needed for transcript access. The full meeting object is large and can include the invite HTML and the meeting passcode, so pass select=id,subject,joinWebUrl unless you need more."
|
|
2085
2085
|
},
|
|
2086
2086
|
{
|
|
2087
2087
|
"pathPattern": "/me/onlineMeetings/{onlineMeeting-id}",
|
|
@@ -2089,7 +2089,7 @@
|
|
|
2089
2089
|
"toolName": "get-online-meeting",
|
|
2090
2090
|
"presets": ["teams", "work"],
|
|
2091
2091
|
"workScopes": ["OnlineMeetings.Read"],
|
|
2092
|
-
"llmTip": "Gets a specific online meeting by ID. Returns full details including subject, startDateTime, endDateTime, joinWebUrl, chatInfo (threadId for meeting chat), participants (organizer, attendees), lobbyBypassSettings, and videoTeleconferenceId. Use list-online-meetings first to find the meeting ID."
|
|
2092
|
+
"llmTip": "Gets a specific online meeting by ID. Returns full details including subject, startDateTime, endDateTime, joinWebUrl, chatInfo (threadId for meeting chat), participants (organizer, attendees), lobbyBypassSettings, and videoTeleconferenceId. Use list-online-meetings first to find the meeting ID. The full meeting object is large and can include the invite HTML and the meeting passcode, so pass select with the fields you came here for, e.g. select=id,chatInfo,participants."
|
|
2093
2093
|
},
|
|
2094
2094
|
{
|
|
2095
2095
|
"pathPattern": "/me/onlineMeetings",
|
package/dist/graph-client.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
loadResilienceConfig
|
|
9
9
|
} from "./lib/graph-resilience.js";
|
|
10
10
|
import { applyMessageSignoffToRequest } from "./lib/message-signoff.js";
|
|
11
|
+
import { TRANSPORT_OK_MESSAGE } from "./lib/select-projection.js";
|
|
11
12
|
import { open, stat, unlink } from "fs/promises";
|
|
12
13
|
import { pipeline } from "stream/promises";
|
|
13
14
|
function isBinaryContentType(contentType) {
|
|
@@ -118,7 +119,7 @@ class GraphClient {
|
|
|
118
119
|
if (isBinaryResponse) {
|
|
119
120
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
120
121
|
result = {
|
|
121
|
-
message:
|
|
122
|
+
message: TRANSPORT_OK_MESSAGE,
|
|
122
123
|
contentType: contentTypeHeader,
|
|
123
124
|
encoding: "base64",
|
|
124
125
|
contentLength: buffer.byteLength,
|
|
@@ -127,14 +128,14 @@ class GraphClient {
|
|
|
127
128
|
} else {
|
|
128
129
|
const text = await response.text();
|
|
129
130
|
if (text === "") {
|
|
130
|
-
result = { message:
|
|
131
|
+
result = { message: TRANSPORT_OK_MESSAGE };
|
|
131
132
|
} else if (options.rawResponse) {
|
|
132
|
-
result = { message:
|
|
133
|
+
result = { message: TRANSPORT_OK_MESSAGE, rawResponse: text };
|
|
133
134
|
} else {
|
|
134
135
|
try {
|
|
135
136
|
result = JSON.parse(text);
|
|
136
137
|
} catch {
|
|
137
|
-
result = { message:
|
|
138
|
+
result = { message: TRANSPORT_OK_MESSAGE, rawResponse: text };
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
}
|
package/dist/graph-tools.js
CHANGED
|
@@ -4,6 +4,12 @@ import logger from "./logger.js";
|
|
|
4
4
|
import { auditLog, getUserIdentityForAudit } from "./audit-log.js";
|
|
5
5
|
import { isDestructiveOperation } from "./lib/destructive-ops.js";
|
|
6
6
|
import { describePathParam } from "./lib/path-params.js";
|
|
7
|
+
import {
|
|
8
|
+
anyFieldPresent,
|
|
9
|
+
isTransportEnvelope,
|
|
10
|
+
parseSelectFields,
|
|
11
|
+
projectSelectedFields
|
|
12
|
+
} from "./lib/select-projection.js";
|
|
7
13
|
import {
|
|
8
14
|
getEndpointScopeGroups,
|
|
9
15
|
getMissingAllowedScopesForGroups,
|
|
@@ -1060,6 +1066,10 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
1060
1066
|
headers["Accept"] = config.acceptType;
|
|
1061
1067
|
logger.info(`Setting custom Accept: ${config.acceptType}`);
|
|
1062
1068
|
}
|
|
1069
|
+
const requestedSelect = parseSelectFields(queryParams["$select"]);
|
|
1070
|
+
const keepFields = [
|
|
1071
|
+
.../* @__PURE__ */ new Set([...requestedSelect, ...parseSelectFields(queryParams["$expand"])])
|
|
1072
|
+
];
|
|
1063
1073
|
if (Object.keys(queryParams).length > 0) {
|
|
1064
1074
|
const queryString = Object.entries(queryParams).map(([key, value]) => `${key}=${encodeURIComponent(value).replace(/%2C/gi, ",")}`).join("&");
|
|
1065
1075
|
path2 = `${path2}${path2.includes("?") ? "&" : "?"}${queryString}`;
|
|
@@ -1123,10 +1133,26 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
1123
1133
|
);
|
|
1124
1134
|
}
|
|
1125
1135
|
const mergePages = fetchAllPages && paginationEnabled;
|
|
1126
|
-
|
|
1136
|
+
const willProject = requestedSelect.length > 0 && params.excludeResponse !== true;
|
|
1137
|
+
if (mergePages || willProject) {
|
|
1127
1138
|
options.forceJsonOutput = true;
|
|
1128
1139
|
}
|
|
1129
1140
|
let response = await graphClient.graphRequest(path2, options);
|
|
1141
|
+
const shouldProject = willProject && !response?.isError;
|
|
1142
|
+
let projectionHandled = false;
|
|
1143
|
+
const applyProjection = (body2) => {
|
|
1144
|
+
if (isTransportEnvelope(body2)) {
|
|
1145
|
+
logger.info("Skipping $select projection: body is a transport envelope, not a resource");
|
|
1146
|
+
return body2;
|
|
1147
|
+
}
|
|
1148
|
+
if (!anyFieldPresent(body2, requestedSelect)) {
|
|
1149
|
+
logger.warn(
|
|
1150
|
+
`None of the requested $select fields (${requestedSelect.join(",")}) appear in the response; returning it untrimmed`
|
|
1151
|
+
);
|
|
1152
|
+
return body2;
|
|
1153
|
+
}
|
|
1154
|
+
return projectSelectedFields(body2, keepFields);
|
|
1155
|
+
};
|
|
1130
1156
|
if (mergePages && response?.content?.[0]?.text) {
|
|
1131
1157
|
let combinedResponse;
|
|
1132
1158
|
try {
|
|
@@ -1190,7 +1216,16 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
1190
1216
|
logger.error(`Error during pagination: ${e}`);
|
|
1191
1217
|
}
|
|
1192
1218
|
if (combinedResponse !== void 0) {
|
|
1193
|
-
|
|
1219
|
+
const merged = shouldProject ? applyProjection(combinedResponse) : combinedResponse;
|
|
1220
|
+
projectionHandled = shouldProject;
|
|
1221
|
+
response.content[0].text = graphClient.serialize(merged);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (shouldProject && !projectionHandled && !response?.isError && response?.content?.[0]?.text) {
|
|
1225
|
+
try {
|
|
1226
|
+
const parsed = JSON.parse(response.content[0].text);
|
|
1227
|
+
response.content[0].text = graphClient.serialize(applyProjection(parsed));
|
|
1228
|
+
} catch {
|
|
1194
1229
|
}
|
|
1195
1230
|
}
|
|
1196
1231
|
if (response?.content?.[0]?.text) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
function splitTopLevel(value) {
|
|
2
|
+
const parts = [];
|
|
3
|
+
let depth = 0;
|
|
4
|
+
let current = "";
|
|
5
|
+
for (const char of value) {
|
|
6
|
+
if (char === "(") depth++;
|
|
7
|
+
else if (char === ")") depth = Math.max(0, depth - 1);
|
|
8
|
+
if (char === "," && depth === 0) {
|
|
9
|
+
parts.push(current);
|
|
10
|
+
current = "";
|
|
11
|
+
} else {
|
|
12
|
+
current += char;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
parts.push(current);
|
|
16
|
+
return parts;
|
|
17
|
+
}
|
|
18
|
+
function parseSelectFields(value) {
|
|
19
|
+
if (!value) return [];
|
|
20
|
+
return [
|
|
21
|
+
...new Set(
|
|
22
|
+
splitTopLevel(value).map((field) => field.trim().split(/[/()]/)[0].trim()).filter(Boolean)
|
|
23
|
+
)
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
function isAlwaysKept(key) {
|
|
27
|
+
return key.toLowerCase() === "id" || key.startsWith("@") || key.startsWith("_");
|
|
28
|
+
}
|
|
29
|
+
const TRANSPORT_OK_MESSAGE = "OK!";
|
|
30
|
+
function isTransportEnvelope(data) {
|
|
31
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return false;
|
|
32
|
+
const body = data;
|
|
33
|
+
if (body.message === TRANSPORT_OK_MESSAGE) return true;
|
|
34
|
+
return body.success === true && Object.keys(body).length === 1;
|
|
35
|
+
}
|
|
36
|
+
function candidateObjects(data) {
|
|
37
|
+
if (Array.isArray(data)) return data.filter(isPlainObject);
|
|
38
|
+
if (!isPlainObject(data)) return [];
|
|
39
|
+
if (Array.isArray(data.value)) return data.value.filter(isPlainObject);
|
|
40
|
+
return [data];
|
|
41
|
+
}
|
|
42
|
+
function isPlainObject(value) {
|
|
43
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
function anyFieldPresent(data, fields) {
|
|
46
|
+
const checkable = fields.filter((field) => !isAlwaysKept(field));
|
|
47
|
+
if (checkable.length === 0) return true;
|
|
48
|
+
const wanted = new Set(checkable.map((field) => field.toLowerCase()));
|
|
49
|
+
const objects = candidateObjects(data);
|
|
50
|
+
if (objects.length === 0) return true;
|
|
51
|
+
return objects.some((item) => Object.keys(item).some((key) => wanted.has(key.toLowerCase())));
|
|
52
|
+
}
|
|
53
|
+
function projectObject(item, wanted) {
|
|
54
|
+
if (!isPlainObject(item)) return item;
|
|
55
|
+
const projected = {};
|
|
56
|
+
for (const [key, value] of Object.entries(item)) {
|
|
57
|
+
if (isAlwaysKept(key) || wanted.has(key.toLowerCase())) {
|
|
58
|
+
projected[key] = value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return projected;
|
|
62
|
+
}
|
|
63
|
+
function projectSelectedFields(data, fields) {
|
|
64
|
+
if (fields.length === 0) return data;
|
|
65
|
+
if (!data || typeof data !== "object") return data;
|
|
66
|
+
const wanted = new Set(fields.map((field) => field.toLowerCase()));
|
|
67
|
+
if (Array.isArray(data)) {
|
|
68
|
+
return data.map((item) => projectObject(item, wanted));
|
|
69
|
+
}
|
|
70
|
+
const body = data;
|
|
71
|
+
if (Array.isArray(body.value)) {
|
|
72
|
+
return { ...body, value: body.value.map((item) => projectObject(item, wanted)) };
|
|
73
|
+
}
|
|
74
|
+
return projectObject(body, wanted);
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
TRANSPORT_OK_MESSAGE,
|
|
78
|
+
anyFieldPresent,
|
|
79
|
+
isTransportEnvelope,
|
|
80
|
+
parseSelectFields,
|
|
81
|
+
projectSelectedFields
|
|
82
|
+
};
|
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.
|
|
4
|
+
"version": "0.150.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",
|
package/src/endpoints.json
CHANGED
|
@@ -510,7 +510,7 @@
|
|
|
510
510
|
"scopes": ["Calendars.Read"],
|
|
511
511
|
"supportsTimezone": true,
|
|
512
512
|
"supportsExpandExtendedProperties": true,
|
|
513
|
-
"llmTip": "Returns expanded recurring event instances (not just seriesMaster) within a date range for the default calendar. Requires startDateTime and endDateTime query parameters in ISO 8601 format (e.g., 2024-01-01T00:00:00Z). Use get-specific-calendar-view if you need a non-default calendar. To find Teams meetings, use $
|
|
513
|
+
"llmTip": "Returns expanded recurring event instances (not just seriesMaster) within a date range for the default calendar. Requires startDateTime and endDateTime query parameters in ISO 8601 format (e.g., 2024-01-01T00:00:00Z). Use get-specific-calendar-view if you need a non-default calendar. To find Teams meetings, use $select=subject,start,isOnlineMeeting,onlineMeetingProvider,onlineMeeting and keep the events with isOnlineMeeting true and onlineMeetingProvider teamsForBusiness; neither property is filterable (isOnlineMeeting returns 400 ErrorInvalidProperty). To search by subject, use $filter=contains(subject,'keyword'). Teams meetings expose their join link as onlineMeeting/joinUrl; the event resource has no joinWebUrl property. Pass that joinUrl to list-online-meetings as $filter=JoinWebUrl eq '{url}' to reach transcripts."
|
|
514
514
|
},
|
|
515
515
|
{
|
|
516
516
|
"pathPattern": "/me/calendars/{calendar-id}/calendarView",
|
|
@@ -521,7 +521,7 @@
|
|
|
521
521
|
"scopes": ["Calendars.Read"],
|
|
522
522
|
"supportsTimezone": true,
|
|
523
523
|
"supportsExpandExtendedProperties": true,
|
|
524
|
-
"llmTip": "Returns expanded recurring event instances (not just seriesMaster) within a date range for a specific calendar. Requires startDateTime and endDateTime query parameters in ISO 8601 format (e.g., 2024-01-01T00:00:00Z). Each instance includes seriesMasterId and type (occurrence/exception) fields for recurring event linkage. Use fetchAllPages=true to retrieve all results when there are many events. To find Teams meetings, use $
|
|
524
|
+
"llmTip": "Returns expanded recurring event instances (not just seriesMaster) within a date range for a specific calendar. Requires startDateTime and endDateTime query parameters in ISO 8601 format (e.g., 2024-01-01T00:00:00Z). Each instance includes seriesMasterId and type (occurrence/exception) fields for recurring event linkage. Use fetchAllPages=true to retrieve all results when there are many events. To find Teams meetings, use $select=subject,start,isOnlineMeeting,onlineMeetingProvider,onlineMeeting and keep the events with isOnlineMeeting true and onlineMeetingProvider teamsForBusiness; neither property is filterable (isOnlineMeeting returns 400 ErrorInvalidProperty). Teams meetings expose their join link as onlineMeeting/joinUrl; the event resource has no joinWebUrl property. Pass that joinUrl to list-online-meetings as $filter=JoinWebUrl eq '{url}' to reach transcripts."
|
|
525
525
|
},
|
|
526
526
|
{
|
|
527
527
|
"pathPattern": "/me/calendar/getSchedule",
|
|
@@ -2081,7 +2081,7 @@
|
|
|
2081
2081
|
"toolName": "list-online-meetings",
|
|
2082
2082
|
"presets": ["teams", "work"],
|
|
2083
2083
|
"workScopes": ["OnlineMeetings.Read"],
|
|
2084
|
-
"llmTip": "Look up a meeting with $filter=JoinWebUrl eq '{url}', using the joinUrl from an event's onlineMeeting property. This filter is required: the collection cannot be listed or paged, and $search/$orderby are not supported. Without it Graph returns 400 InvalidArgument ('One of the required parameters to lookup meeting by QueryOptions is null or empty'). Returns the meeting ID needed for transcript access."
|
|
2084
|
+
"llmTip": "Look up a meeting with $filter=JoinWebUrl eq '{url}', using the joinUrl from an event's onlineMeeting property. This filter is required: the collection cannot be listed or paged, and $search/$orderby are not supported. Without it Graph returns 400 InvalidArgument ('One of the required parameters to lookup meeting by QueryOptions is null or empty'). Returns the meeting ID needed for transcript access. The full meeting object is large and can include the invite HTML and the meeting passcode, so pass select=id,subject,joinWebUrl unless you need more."
|
|
2085
2085
|
},
|
|
2086
2086
|
{
|
|
2087
2087
|
"pathPattern": "/me/onlineMeetings/{onlineMeeting-id}",
|
|
@@ -2089,7 +2089,7 @@
|
|
|
2089
2089
|
"toolName": "get-online-meeting",
|
|
2090
2090
|
"presets": ["teams", "work"],
|
|
2091
2091
|
"workScopes": ["OnlineMeetings.Read"],
|
|
2092
|
-
"llmTip": "Gets a specific online meeting by ID. Returns full details including subject, startDateTime, endDateTime, joinWebUrl, chatInfo (threadId for meeting chat), participants (organizer, attendees), lobbyBypassSettings, and videoTeleconferenceId. Use list-online-meetings first to find the meeting ID."
|
|
2092
|
+
"llmTip": "Gets a specific online meeting by ID. Returns full details including subject, startDateTime, endDateTime, joinWebUrl, chatInfo (threadId for meeting chat), participants (organizer, attendees), lobbyBypassSettings, and videoTeleconferenceId. Use list-online-meetings first to find the meeting ID. The full meeting object is large and can include the invite HTML and the meeting passcode, so pass select with the fields you came here for, e.g. select=id,chatInfo,participants."
|
|
2093
2093
|
},
|
|
2094
2094
|
{
|
|
2095
2095
|
"pathPattern": "/me/onlineMeetings",
|