@softeria/ms-365-mcp-server 0.157.2 → 0.158.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 +74 -0
- package/dist/__tests__/graph-tools.test.js +407 -120
- package/dist/cli.js +13 -1
- package/dist/graph-tools.js +195 -53
- package/dist/lib/select-projection.js +9 -8
- package/dist/lib/user-field-policy.js +69 -0
- package/dist/server.js +32 -43
- package/package.json +1 -1
package/dist/graph-tools.js
CHANGED
|
@@ -1,58 +1,66 @@
|
|
|
1
1
|
import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
|
-
import
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { access } from "fs/promises";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { z } from "zod";
|
|
4
8
|
import { auditLog, getUserIdentityForAudit } from "./audit-log.js";
|
|
5
|
-
import {
|
|
6
|
-
import { describePathParam } from "./lib/path-params.js";
|
|
7
|
-
import { getAttachmentMinting } from "./lib/attachment-minting.js";
|
|
8
|
-
import {
|
|
9
|
-
buildAttachmentUrl,
|
|
10
|
-
isPlainGraphPath,
|
|
11
|
-
TicketStoreFullError
|
|
12
|
-
} from "./lib/attachment-tickets.js";
|
|
13
|
-
import {
|
|
14
|
-
anyFieldPresent,
|
|
15
|
-
isTransportEnvelope,
|
|
16
|
-
parseSelectFields,
|
|
17
|
-
projectSelectedFields
|
|
18
|
-
} from "./lib/select-projection.js";
|
|
9
|
+
import { deriveTargetResource } from "./audit-target-resource.js";
|
|
19
10
|
import {
|
|
20
11
|
getEndpointScopeGroups,
|
|
21
12
|
getMissingAllowedScopesForGroups,
|
|
22
13
|
parseAllowedScopes
|
|
23
14
|
} from "./auth.js";
|
|
24
|
-
import { api } from "./generated/client.js";
|
|
25
15
|
import { api as betaApi } from "./generated/client-beta.js";
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
import { getRequestTokens } from "./request-context.js";
|
|
34
|
-
import { parseTeamsUrl } from "./lib/teams-url-parser.js";
|
|
16
|
+
import { api } from "./generated/client.js";
|
|
17
|
+
import { getAttachmentMinting } from "./lib/attachment-minting.js";
|
|
18
|
+
import {
|
|
19
|
+
buildAttachmentUrl,
|
|
20
|
+
isPlainGraphPath,
|
|
21
|
+
TicketStoreFullError
|
|
22
|
+
} from "./lib/attachment-tickets.js";
|
|
35
23
|
import { buildBM25Index, scoreQuery, tokenize } from "./lib/bm25.js";
|
|
36
|
-
import {
|
|
37
|
-
import { describeToolSchema, describeUtilityToolSchema } from "./lib/tool-schema.js";
|
|
38
|
-
import { queryParameterSchema } from "./lib/query-parameter-schema.js";
|
|
24
|
+
import { isDestructiveOperation } from "./lib/destructive-ops.js";
|
|
39
25
|
import {
|
|
40
|
-
TOP_UNSUPPORTED_DELTA_TOOLS,
|
|
41
|
-
shouldOmitTopParam,
|
|
42
|
-
paginationAllowed,
|
|
43
|
-
positiveIntFromEnv,
|
|
44
|
-
DEFAULT_MAX_PAGES,
|
|
45
|
-
getMaxPages,
|
|
46
|
-
isFetchAllPagesApplicable,
|
|
47
26
|
CONFIRM_PARAM_DESCRIPTION,
|
|
48
|
-
|
|
27
|
+
DEFAULT_MAX_PAGES,
|
|
49
28
|
EXPAND_EXTENDED_PROPERTIES_PARAM_DESCRIPTION,
|
|
50
29
|
getAcceptParamDescription,
|
|
51
30
|
getAccountParamDescription,
|
|
52
31
|
getFetchAllPagesParamDescription,
|
|
32
|
+
getMaxPages,
|
|
33
|
+
isFetchAllPagesApplicable,
|
|
34
|
+
isSkiptokenApplicable,
|
|
35
|
+
paginationAllowed,
|
|
36
|
+
positiveIntFromEnv,
|
|
37
|
+
shouldOmitTopParam,
|
|
53
38
|
SKIPTOKEN_PARAM_DESCRIPTION,
|
|
54
|
-
|
|
39
|
+
TIMEZONE_PARAM_DESCRIPTION,
|
|
40
|
+
TOP_UNSUPPORTED_DELTA_TOOLS
|
|
55
41
|
} from "./lib/param-descriptions.js";
|
|
42
|
+
import { describePathParam } from "./lib/path-params.js";
|
|
43
|
+
import { queryParameterSchema } from "./lib/query-parameter-schema.js";
|
|
44
|
+
import {
|
|
45
|
+
anyFieldPresent,
|
|
46
|
+
isTransportEnvelope,
|
|
47
|
+
parseSelectFields,
|
|
48
|
+
projectSelectedFields
|
|
49
|
+
} from "./lib/select-projection.js";
|
|
50
|
+
import { parseTeamsUrl } from "./lib/teams-url-parser.js";
|
|
51
|
+
import { describeToolSchema, describeUtilityToolSchema } from "./lib/tool-schema.js";
|
|
52
|
+
import {
|
|
53
|
+
restrictUserFieldQuery,
|
|
54
|
+
restrictUserFieldUrl,
|
|
55
|
+
shouldStripUserFieldExpand,
|
|
56
|
+
stripUserFieldExpandFromUrl,
|
|
57
|
+
targetsUserProfile,
|
|
58
|
+
userFieldEnforcement
|
|
59
|
+
} from "./lib/user-field-policy.js";
|
|
60
|
+
import logger from "./logger.js";
|
|
61
|
+
import { getRequestTokens } from "./request-context.js";
|
|
62
|
+
import { TOOL_CATEGORIES } from "./tool-categories.js";
|
|
63
|
+
const allEndpoints = [...api.endpoints, ...betaApi.endpoints];
|
|
56
64
|
const __filename = fileURLToPath(import.meta.url);
|
|
57
65
|
const __dirname = path.dirname(__filename);
|
|
58
66
|
const endpointsData = JSON.parse(
|
|
@@ -81,6 +89,16 @@ function clampTopQueryParam(queryParams) {
|
|
|
81
89
|
logger.info(`Clamping $top from ${requested} to ${cap} (MS365_MCP_MAX_TOP)`);
|
|
82
90
|
queryParams["$top"] = String(cap);
|
|
83
91
|
}
|
|
92
|
+
function parseUserFields(value) {
|
|
93
|
+
if (value === void 0) return void 0;
|
|
94
|
+
const fields = value.split(",").map((field) => field.trim()).filter(Boolean);
|
|
95
|
+
if (fields.length === 0) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"User field allowlist was configured but names no fields. Provide one or more comma-separated Graph fields, or omit --user-fields / MS365_MCP_USER_FIELDS."
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return fields;
|
|
101
|
+
}
|
|
84
102
|
const OUTLOOK_MAIL_PATH = /^\/(?:me|users\/[^/]+)(?:\/(?:mailFolders|childFolders)\/[^/]+)*\/messages(?:\/delta\(\))?$/i;
|
|
85
103
|
function isOutlookMailPath(path2) {
|
|
86
104
|
return OUTLOOK_MAIL_PATH.test(path2);
|
|
@@ -640,7 +658,7 @@ const UTILITY_TOOLS = [
|
|
|
640
658
|
}
|
|
641
659
|
return schema;
|
|
642
660
|
},
|
|
643
|
-
execute: async (params, { graphClient, authManager }) => {
|
|
661
|
+
execute: async (params, { graphClient, authManager, userFields }) => {
|
|
644
662
|
const target = params.target;
|
|
645
663
|
const accountParam = params.account;
|
|
646
664
|
if (typeof target !== "string" || target.length === 0) {
|
|
@@ -667,6 +685,8 @@ const UTILITY_TOOLS = [
|
|
|
667
685
|
isError: true
|
|
668
686
|
};
|
|
669
687
|
}
|
|
688
|
+
const restrictedTarget = userProfilePassthroughError(target, userFields);
|
|
689
|
+
if (restrictedTarget) return restrictedTarget;
|
|
670
690
|
try {
|
|
671
691
|
const accountModeError = await checkAccountParamInBearerMode(accountParam, authManager);
|
|
672
692
|
if (accountModeError) {
|
|
@@ -721,7 +741,7 @@ const UTILITY_TOOLS = [
|
|
|
721
741
|
}
|
|
722
742
|
return schema;
|
|
723
743
|
},
|
|
724
|
-
execute: async (params, { graphClient, authManager }) => {
|
|
744
|
+
execute: async (params, { graphClient, authManager, userFields }) => {
|
|
725
745
|
const target = params.target;
|
|
726
746
|
const outputPath = params.outputPath;
|
|
727
747
|
const accountParam = params.account;
|
|
@@ -762,6 +782,8 @@ const UTILITY_TOOLS = [
|
|
|
762
782
|
isError: true
|
|
763
783
|
};
|
|
764
784
|
}
|
|
785
|
+
const restrictedTarget = userProfilePassthroughError(target, userFields);
|
|
786
|
+
if (restrictedTarget) return restrictedTarget;
|
|
765
787
|
if (!path.isAbsolute(outputPath)) {
|
|
766
788
|
return {
|
|
767
789
|
content: [
|
|
@@ -858,7 +880,7 @@ const UTILITY_TOOLS = [
|
|
|
858
880
|
}
|
|
859
881
|
return schema;
|
|
860
882
|
},
|
|
861
|
-
execute: async (params, { graphClient, authManager }) => {
|
|
883
|
+
execute: async (params, { graphClient, authManager, userFields }) => {
|
|
862
884
|
const target = params.target;
|
|
863
885
|
const accountParam = params.account;
|
|
864
886
|
if (typeof target !== "string" || target.length === 0) {
|
|
@@ -885,6 +907,8 @@ const UTILITY_TOOLS = [
|
|
|
885
907
|
isError: true
|
|
886
908
|
};
|
|
887
909
|
}
|
|
910
|
+
const restrictedTarget = userProfilePassthroughError(target, userFields);
|
|
911
|
+
if (restrictedTarget) return restrictedTarget;
|
|
888
912
|
const queryIdx = target.indexOf("?");
|
|
889
913
|
if (queryIdx >= 0) {
|
|
890
914
|
return {
|
|
@@ -1096,6 +1120,69 @@ const READ_ONLY_BODY_FIELDS = /* @__PURE__ */ new Set([
|
|
|
1096
1120
|
function isPlainObject(value) {
|
|
1097
1121
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1098
1122
|
}
|
|
1123
|
+
function restrictBatchSubrequests(body, allowlist) {
|
|
1124
|
+
const restricted = /* @__PURE__ */ new Map();
|
|
1125
|
+
let changed = false;
|
|
1126
|
+
let parsed = body;
|
|
1127
|
+
const wasString = typeof body === "string";
|
|
1128
|
+
if (wasString) {
|
|
1129
|
+
try {
|
|
1130
|
+
parsed = JSON.parse(body);
|
|
1131
|
+
} catch {
|
|
1132
|
+
return { body, restricted };
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (!isPlainObject(parsed) || !Array.isArray(parsed.requests)) return { body, restricted };
|
|
1136
|
+
parsed.requests.forEach((request, index) => {
|
|
1137
|
+
if (!isPlainObject(request) || typeof request.url !== "string") return;
|
|
1138
|
+
const method = typeof request.method === "string" ? request.method.toUpperCase() : "GET";
|
|
1139
|
+
const enforcement = userFieldEnforcement(request.url);
|
|
1140
|
+
if (method !== "GET") return;
|
|
1141
|
+
if (enforcement !== "none") {
|
|
1142
|
+
const { url, fields } = restrictUserFieldUrl(request.url, allowlist, enforcement);
|
|
1143
|
+
request.url = url;
|
|
1144
|
+
restricted.set(String(request.id ?? index), fields);
|
|
1145
|
+
changed = true;
|
|
1146
|
+
logger.info(`Restricting batch subrequest ${String(request.id ?? index)} to: ${fields}`);
|
|
1147
|
+
} else if (shouldStripUserFieldExpand(request.url)) {
|
|
1148
|
+
const url = stripUserFieldExpandFromUrl(request.url);
|
|
1149
|
+
if (url !== request.url) {
|
|
1150
|
+
request.url = url;
|
|
1151
|
+
changed = true;
|
|
1152
|
+
logger.info(`Removing $expand from batch subrequest ${String(request.id ?? index)}`);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
if (!changed) return { body, restricted };
|
|
1157
|
+
return { body: wasString ? JSON.stringify(parsed) : parsed, restricted };
|
|
1158
|
+
}
|
|
1159
|
+
function projectBatchSubresponses(data, restricted) {
|
|
1160
|
+
if (!isPlainObject(data) || !Array.isArray(data.responses)) return data;
|
|
1161
|
+
return {
|
|
1162
|
+
...data,
|
|
1163
|
+
responses: data.responses.map((subresponse) => {
|
|
1164
|
+
if (!isPlainObject(subresponse)) return subresponse;
|
|
1165
|
+
const fields = restricted.get(String(subresponse.id));
|
|
1166
|
+
if (!fields) return subresponse;
|
|
1167
|
+
return { ...subresponse, body: projectSelectedFields(subresponse.body, fields, true) };
|
|
1168
|
+
})
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
function userProfilePassthroughError(target, userFields) {
|
|
1172
|
+
if (userFields === void 0 || !targetsUserProfile(target)) return void 0;
|
|
1173
|
+
return {
|
|
1174
|
+
content: [
|
|
1175
|
+
{
|
|
1176
|
+
type: "text",
|
|
1177
|
+
text: JSON.stringify({
|
|
1178
|
+
error: "user_fields_restricted",
|
|
1179
|
+
message: "This deployment restricts which user profile fields may be returned, and this tool returns the response bytes verbatim. Use list-users, which applies the restriction."
|
|
1180
|
+
})
|
|
1181
|
+
}
|
|
1182
|
+
],
|
|
1183
|
+
isError: true
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1099
1186
|
function hasOwn(obj, key) {
|
|
1100
1187
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
1101
1188
|
}
|
|
@@ -1106,7 +1193,7 @@ function describeParamsForLog(params) {
|
|
|
1106
1193
|
return `[unserializable: ${error instanceof Error ? error.name : "unknown error"}]`;
|
|
1107
1194
|
}
|
|
1108
1195
|
}
|
|
1109
|
-
async function executeGraphTool(tool, config, graphClient, params, authManager) {
|
|
1196
|
+
async function executeGraphTool(tool, config, graphClient, params, authManager, userFields) {
|
|
1110
1197
|
logger.info(`Tool ${tool.alias} called with params: ${describeParamsForLog(params)}`);
|
|
1111
1198
|
if (isConfirmGateEnabled() && isDestructiveOperation(tool.method, config) && params.confirm !== true) {
|
|
1112
1199
|
logger.warn(
|
|
@@ -1364,8 +1451,26 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
1364
1451
|
logger.info(`Setting custom Accept: ${config.acceptType}`);
|
|
1365
1452
|
}
|
|
1366
1453
|
const requestedSelect = parseSelectFields(queryParams["$select"]);
|
|
1454
|
+
const enforcement = userFields === void 0 ? "none" : userFieldEnforcement(path2);
|
|
1455
|
+
const isUserFieldBoundary = enforcement !== "none";
|
|
1456
|
+
const boundaryFields = isUserFieldBoundary && userFields !== void 0 ? restrictUserFieldQuery(queryParams, userFields, enforcement) : [];
|
|
1457
|
+
if (userFields !== void 0 && shouldStripUserFieldExpand(path2)) {
|
|
1458
|
+
delete queryParams["$expand"];
|
|
1459
|
+
}
|
|
1460
|
+
if (isUserFieldBoundary) {
|
|
1461
|
+
logger.info(
|
|
1462
|
+
`Restricting ${tool.alias} to configured user fields (${enforcement}): ${boundaryFields}`
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
let batchRestricted = /* @__PURE__ */ new Map();
|
|
1466
|
+
if (userFields !== void 0) {
|
|
1467
|
+
const restrictedBatch = restrictBatchSubrequests(body, userFields);
|
|
1468
|
+
body = restrictedBatch.body;
|
|
1469
|
+
batchRestricted = restrictedBatch.restricted;
|
|
1470
|
+
}
|
|
1471
|
+
const projectionSelect = isUserFieldBoundary ? boundaryFields : requestedSelect;
|
|
1367
1472
|
const keepFields = [
|
|
1368
|
-
.../* @__PURE__ */ new Set([...
|
|
1473
|
+
.../* @__PURE__ */ new Set([...projectionSelect, ...parseSelectFields(queryParams["$expand"])])
|
|
1369
1474
|
];
|
|
1370
1475
|
if (Object.keys(queryParams).length > 0) {
|
|
1371
1476
|
const queryString = Object.entries(queryParams).map(([key, value]) => `${key}=${encodeURIComponent(value).replace(/%2C/gi, ",")}`).join("&");
|
|
@@ -1430,8 +1535,9 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
1430
1535
|
);
|
|
1431
1536
|
}
|
|
1432
1537
|
const mergePages = fetchAllPages && paginationEnabled;
|
|
1433
|
-
const willProject = requestedSelect.length > 0 && params.excludeResponse !== true;
|
|
1434
|
-
|
|
1538
|
+
const willProject = (requestedSelect.length > 0 || isUserFieldBoundary) && params.excludeResponse !== true;
|
|
1539
|
+
const willProjectBatch = batchRestricted.size > 0 && params.excludeResponse !== true;
|
|
1540
|
+
if (mergePages || willProject || willProjectBatch) {
|
|
1435
1541
|
options.forceJsonOutput = true;
|
|
1436
1542
|
}
|
|
1437
1543
|
let response = await graphClient.graphRequest(path2, options);
|
|
@@ -1442,13 +1548,13 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
1442
1548
|
logger.info("Skipping $select projection: body is a transport envelope, not a resource");
|
|
1443
1549
|
return body2;
|
|
1444
1550
|
}
|
|
1445
|
-
if (!anyFieldPresent(body2,
|
|
1551
|
+
if (!isUserFieldBoundary && !anyFieldPresent(body2, projectionSelect)) {
|
|
1446
1552
|
logger.warn(
|
|
1447
|
-
`None of the requested $select fields (${
|
|
1553
|
+
`None of the requested $select fields (${projectionSelect.join(",")}) appear in the response; returning it untrimmed`
|
|
1448
1554
|
);
|
|
1449
1555
|
return body2;
|
|
1450
1556
|
}
|
|
1451
|
-
return projectSelectedFields(body2, keepFields);
|
|
1557
|
+
return projectSelectedFields(body2, keepFields, isUserFieldBoundary);
|
|
1452
1558
|
};
|
|
1453
1559
|
if (mergePages && response?.content?.[0]?.text) {
|
|
1454
1560
|
let combinedResponse;
|
|
@@ -1535,6 +1641,15 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
1535
1641
|
} catch {
|
|
1536
1642
|
}
|
|
1537
1643
|
}
|
|
1644
|
+
if (willProjectBatch && !response?.isError && response?.content?.[0]?.text) {
|
|
1645
|
+
try {
|
|
1646
|
+
const parsed = JSON.parse(response.content[0].text);
|
|
1647
|
+
response.content[0].text = graphClient.serialize(
|
|
1648
|
+
projectBatchSubresponses(parsed, batchRestricted)
|
|
1649
|
+
);
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1538
1653
|
if (response?.content?.[0]?.text) {
|
|
1539
1654
|
const responseText = response.content[0].text;
|
|
1540
1655
|
logger.info(`Response size: ${responseText.length} characters`);
|
|
@@ -1599,7 +1714,19 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
1599
1714
|
};
|
|
1600
1715
|
}
|
|
1601
1716
|
}
|
|
1602
|
-
function registerGraphTools(server, graphClient,
|
|
1717
|
+
function registerGraphTools(server, graphClient, options = {}) {
|
|
1718
|
+
const {
|
|
1719
|
+
readOnly = false,
|
|
1720
|
+
enabledTools: enabledToolsPattern,
|
|
1721
|
+
orgMode = false,
|
|
1722
|
+
authManager,
|
|
1723
|
+
multiAccount = false,
|
|
1724
|
+
accountNames = [],
|
|
1725
|
+
allowedScopes: allowedScopesValue,
|
|
1726
|
+
httpMode = false,
|
|
1727
|
+
userFields: userFieldsValue
|
|
1728
|
+
} = options;
|
|
1729
|
+
const userFields = parseUserFields(userFieldsValue);
|
|
1603
1730
|
let enabledToolsRegex;
|
|
1604
1731
|
if (enabledToolsPattern) {
|
|
1605
1732
|
try {
|
|
@@ -1718,7 +1845,7 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
1718
1845
|
// All tools call Microsoft Graph API
|
|
1719
1846
|
}
|
|
1720
1847
|
},
|
|
1721
|
-
async (params) => executeGraphTool(tool, endpointConfig, graphClient, params, authManager)
|
|
1848
|
+
async (params) => executeGraphTool(tool, endpointConfig, graphClient, params, authManager, userFields)
|
|
1722
1849
|
);
|
|
1723
1850
|
registeredCount++;
|
|
1724
1851
|
} catch (error) {
|
|
@@ -1738,7 +1865,8 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
1738
1865
|
graphClient,
|
|
1739
1866
|
authManager,
|
|
1740
1867
|
multiAccount,
|
|
1741
|
-
accountNames
|
|
1868
|
+
accountNames,
|
|
1869
|
+
userFields
|
|
1742
1870
|
};
|
|
1743
1871
|
for (const utility of UTILITY_TOOLS) {
|
|
1744
1872
|
if (readOnly && !utility.readOnlyHint) continue;
|
|
@@ -1859,7 +1987,19 @@ function scoreDiscoveryQuery(query, index) {
|
|
|
1859
1987
|
ranked.sort((a, b) => b.score - a.score);
|
|
1860
1988
|
return ranked;
|
|
1861
1989
|
}
|
|
1862
|
-
function registerDiscoveryTools(server, graphClient,
|
|
1990
|
+
function registerDiscoveryTools(server, graphClient, options = {}) {
|
|
1991
|
+
const {
|
|
1992
|
+
readOnly = false,
|
|
1993
|
+
enabledTools,
|
|
1994
|
+
orgMode = false,
|
|
1995
|
+
authManager,
|
|
1996
|
+
multiAccount = false,
|
|
1997
|
+
accountNames = [],
|
|
1998
|
+
allowedScopes: allowedScopesValue,
|
|
1999
|
+
httpMode = false,
|
|
2000
|
+
userFields: userFieldsValue
|
|
2001
|
+
} = options;
|
|
2002
|
+
const userFields = parseUserFields(userFieldsValue);
|
|
1863
2003
|
let enabledToolsRegex;
|
|
1864
2004
|
if (enabledTools) {
|
|
1865
2005
|
try {
|
|
@@ -1906,7 +2046,8 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
|
|
|
1906
2046
|
graphClient,
|
|
1907
2047
|
authManager,
|
|
1908
2048
|
multiAccount,
|
|
1909
|
-
accountNames
|
|
2049
|
+
accountNames,
|
|
2050
|
+
userFields
|
|
1910
2051
|
};
|
|
1911
2052
|
const utilityByName = new Map(utilityTools.map((u) => [u.name, u]));
|
|
1912
2053
|
const categoryNames = Object.keys(TOOL_CATEGORIES).join(", ");
|
|
@@ -2047,7 +2188,8 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
|
|
|
2047
2188
|
toolData.config,
|
|
2048
2189
|
graphClient,
|
|
2049
2190
|
parameters,
|
|
2050
|
-
authManager
|
|
2191
|
+
authManager,
|
|
2192
|
+
userFields
|
|
2051
2193
|
);
|
|
2052
2194
|
}
|
|
2053
2195
|
const utility = utilityByName.get(tool_name);
|
|
@@ -23,8 +23,9 @@ function parseSelectFields(value) {
|
|
|
23
23
|
)
|
|
24
24
|
];
|
|
25
25
|
}
|
|
26
|
-
function isAlwaysKept(key) {
|
|
27
|
-
|
|
26
|
+
function isAlwaysKept(key, strict = false) {
|
|
27
|
+
if (key.startsWith("@") || key.startsWith("_")) return true;
|
|
28
|
+
return !strict && key.toLowerCase() === "id";
|
|
28
29
|
}
|
|
29
30
|
const TRANSPORT_OK_MESSAGE = "OK!";
|
|
30
31
|
function isTransportEnvelope(data) {
|
|
@@ -50,28 +51,28 @@ function anyFieldPresent(data, fields) {
|
|
|
50
51
|
if (objects.length === 0) return true;
|
|
51
52
|
return objects.some((item) => Object.keys(item).some((key) => wanted.has(key.toLowerCase())));
|
|
52
53
|
}
|
|
53
|
-
function projectObject(item, wanted) {
|
|
54
|
+
function projectObject(item, wanted, strict) {
|
|
54
55
|
if (!isPlainObject(item)) return item;
|
|
55
56
|
const projected = {};
|
|
56
57
|
for (const [key, value] of Object.entries(item)) {
|
|
57
|
-
if (isAlwaysKept(key) || wanted.has(key.toLowerCase())) {
|
|
58
|
+
if (isAlwaysKept(key, strict) || wanted.has(key.toLowerCase())) {
|
|
58
59
|
projected[key] = value;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
return projected;
|
|
62
63
|
}
|
|
63
|
-
function projectSelectedFields(data, fields) {
|
|
64
|
+
function projectSelectedFields(data, fields, strict = false) {
|
|
64
65
|
if (fields.length === 0) return data;
|
|
65
66
|
if (!data || typeof data !== "object") return data;
|
|
66
67
|
const wanted = new Set(fields.map((field) => field.toLowerCase()));
|
|
67
68
|
if (Array.isArray(data)) {
|
|
68
|
-
return data.map((item) => projectObject(item, wanted));
|
|
69
|
+
return data.map((item) => projectObject(item, wanted, strict));
|
|
69
70
|
}
|
|
70
71
|
const body = data;
|
|
71
72
|
if (Array.isArray(body.value)) {
|
|
72
|
-
return { ...body, value: body.value.map((item) => projectObject(item, wanted)) };
|
|
73
|
+
return { ...body, value: body.value.map((item) => projectObject(item, wanted, strict)) };
|
|
73
74
|
}
|
|
74
|
-
return projectObject(body, wanted);
|
|
75
|
+
return projectObject(body, wanted, strict);
|
|
75
76
|
}
|
|
76
77
|
export {
|
|
77
78
|
TRANSPORT_OK_MESSAGE,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { parseSelectFields } from "./select-projection.js";
|
|
2
|
+
const USER_ENTITY_PATH = /^\/users(?:\/[^/]+)?\/?$/i;
|
|
3
|
+
const DIRECTORY_NAVIGATION_PATH = /^(?:\/me|\/users\/[^/]+)\/(?:manager|directReports)\/?$|^\/groups\/[^/]+\/(?:members|owners)\/?$/i;
|
|
4
|
+
const EXPAND_USER_PATH = /^\/me\/?$|^\/groups(?:\/[^/]+)?\/?$/i;
|
|
5
|
+
function normalizePath(path) {
|
|
6
|
+
const withoutQuery = path.split("?")[0];
|
|
7
|
+
return withoutQuery.startsWith("/") ? withoutQuery : `/${withoutQuery}`;
|
|
8
|
+
}
|
|
9
|
+
function userFieldEnforcement(path) {
|
|
10
|
+
const normalized = normalizePath(path);
|
|
11
|
+
if (USER_ENTITY_PATH.test(normalized)) return "request-and-response";
|
|
12
|
+
if (DIRECTORY_NAVIGATION_PATH.test(normalized)) return "response-only";
|
|
13
|
+
return "none";
|
|
14
|
+
}
|
|
15
|
+
function targetsUserProfile(path) {
|
|
16
|
+
return userFieldEnforcement(path) !== "none";
|
|
17
|
+
}
|
|
18
|
+
function shouldStripUserFieldExpand(path) {
|
|
19
|
+
const normalized = normalizePath(path);
|
|
20
|
+
return userFieldEnforcement(normalized) !== "none" || EXPAND_USER_PATH.test(normalized);
|
|
21
|
+
}
|
|
22
|
+
function stripUserFieldExpandFromUrl(url) {
|
|
23
|
+
const [path, query = ""] = url.split("?");
|
|
24
|
+
if (!shouldStripUserFieldExpand(path) || query === "") return url;
|
|
25
|
+
const params = new URLSearchParams(query);
|
|
26
|
+
if (!params.has("$expand")) return url;
|
|
27
|
+
params.delete("$expand");
|
|
28
|
+
const rebuilt = params.toString();
|
|
29
|
+
if (rebuilt === "") return path;
|
|
30
|
+
return `${path}?${rebuilt.replace(/%24/gi, "$").replace(/%2C/gi, ",")}`;
|
|
31
|
+
}
|
|
32
|
+
function effectiveUserFields(requestedSelect, allowlist) {
|
|
33
|
+
const allowed = requestedSelect.filter(
|
|
34
|
+
(field) => allowlist.some((entry) => entry.toLowerCase() === field.toLowerCase())
|
|
35
|
+
);
|
|
36
|
+
return allowed.length > 0 ? allowed : allowlist;
|
|
37
|
+
}
|
|
38
|
+
function restrictUserFieldQuery(queryParams, allowlist, enforcement) {
|
|
39
|
+
const fields = effectiveUserFields(parseSelectFields(queryParams["$select"]), allowlist);
|
|
40
|
+
if (enforcement === "request-and-response") {
|
|
41
|
+
queryParams["$select"] = fields.join(",");
|
|
42
|
+
}
|
|
43
|
+
delete queryParams["$expand"];
|
|
44
|
+
return fields;
|
|
45
|
+
}
|
|
46
|
+
function restrictUserFieldUrl(url, allowlist, enforcement) {
|
|
47
|
+
const [path, query = ""] = url.split("?");
|
|
48
|
+
const params = new URLSearchParams(query);
|
|
49
|
+
const fields = effectiveUserFields(
|
|
50
|
+
parseSelectFields(params.get("$select") ?? void 0),
|
|
51
|
+
allowlist
|
|
52
|
+
);
|
|
53
|
+
if (enforcement === "request-and-response") {
|
|
54
|
+
params.set("$select", fields.join(","));
|
|
55
|
+
}
|
|
56
|
+
params.delete("$expand");
|
|
57
|
+
const rebuilt = params.toString();
|
|
58
|
+
if (rebuilt === "") return { url: path, fields };
|
|
59
|
+
return { url: `${path}?${rebuilt.replace(/%24/gi, "$").replace(/%2C/gi, ",")}`, fields };
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
effectiveUserFields,
|
|
63
|
+
restrictUserFieldQuery,
|
|
64
|
+
restrictUserFieldUrl,
|
|
65
|
+
shouldStripUserFieldExpand,
|
|
66
|
+
stripUserFieldExpandFromUrl,
|
|
67
|
+
targetsUserProfile,
|
|
68
|
+
userFieldEnforcement
|
|
69
|
+
};
|
package/dist/server.js
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
|
+
import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { hostHeaderValidation } from "@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js";
|
|
2
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
5
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
-
import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
5
6
|
import express from "express";
|
|
6
|
-
import helmet from "helmet";
|
|
7
7
|
import rateLimit from "express-rate-limit";
|
|
8
|
-
import
|
|
8
|
+
import helmet from "helmet";
|
|
9
|
+
import crypto from "node:crypto";
|
|
10
|
+
import { isIP, isIPv6 } from "node:net";
|
|
11
|
+
import { createAttachmentHandler } from "./attachment-route.js";
|
|
9
12
|
import { registerAuthTools } from "./auth-tools.js";
|
|
10
|
-
import { registerGraphTools, registerDiscoveryTools } from "./graph-tools.js";
|
|
11
|
-
import { buildMcpServerInstructions } from "./mcp-instructions.js";
|
|
12
|
-
import { installToolSchemaRefNormalization } from "./normalize-tool-schema.js";
|
|
13
|
-
import GraphClient from "./graph-client.js";
|
|
14
13
|
import {
|
|
15
14
|
buildScopesFromEndpoints,
|
|
16
15
|
parseAllowedScopes,
|
|
17
16
|
resolveAuthScopes
|
|
18
17
|
} from "./auth.js";
|
|
19
|
-
import {
|
|
18
|
+
import { getCloudEndpoints } from "./cloud-config.js";
|
|
19
|
+
import { dumpError } from "./crash-logging.js";
|
|
20
|
+
import GraphClient from "./graph-client.js";
|
|
21
|
+
import { registerDiscoveryTools, registerGraphTools } from "./graph-tools.js";
|
|
22
|
+
import { configureAttachmentMinting } from "./lib/attachment-minting.js";
|
|
23
|
+
import { AttachmentTicketStore } from "./lib/attachment-tickets.js";
|
|
24
|
+
import { ATTACHMENT_ROUTE, loadAttachmentUrlConfig } from "./lib/attachment-url-config.js";
|
|
20
25
|
import {
|
|
21
26
|
exchangeCodeForToken,
|
|
22
27
|
microsoftBearerTokenAuthMiddleware,
|
|
@@ -25,18 +30,13 @@ import {
|
|
|
25
30
|
toOAuthErrorResponse
|
|
26
31
|
} from "./lib/microsoft-auth.js";
|
|
27
32
|
import { isAllowedRedirectUri, parseAllowlist } from "./lib/redirect-uri-validation.js";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import { getSecrets } from "./secrets.js";
|
|
33
|
-
import { getCloudEndpoints } from "./cloud-config.js";
|
|
34
|
-
import { requestContext } from "./request-context.js";
|
|
35
|
-
import { dumpError } from "./crash-logging.js";
|
|
36
|
-
import crypto from "node:crypto";
|
|
37
|
-
import { isIP, isIPv6 } from "node:net";
|
|
33
|
+
import logger, { enableConsoleLogging } from "./logger.js";
|
|
34
|
+
import { buildMcpServerInstructions } from "./mcp-instructions.js";
|
|
35
|
+
import { installToolSchemaRefNormalization } from "./normalize-tool-schema.js";
|
|
36
|
+
import { MicrosoftOAuthProvider } from "./oauth-provider.js";
|
|
38
37
|
import OboClient from "./obo-client.js";
|
|
39
|
-
import {
|
|
38
|
+
import { requestContext } from "./request-context.js";
|
|
39
|
+
import { getSecrets } from "./secrets.js";
|
|
40
40
|
const LOOPBACK_HOSTNAMES = ["localhost", "127.0.0.1", "[::1]"];
|
|
41
41
|
function isLoopbackHost(host) {
|
|
42
42
|
if (!host) return false;
|
|
@@ -187,32 +187,21 @@ class MicrosoftGraphServer {
|
|
|
187
187
|
if (shouldRegisterAuthTools) {
|
|
188
188
|
registerAuthTools(server, this.authManager);
|
|
189
189
|
}
|
|
190
|
+
const registrationOptions = {
|
|
191
|
+
readOnly: this.options.readOnly,
|
|
192
|
+
enabledTools: this.options.enabledTools,
|
|
193
|
+
orgMode: this.options.orgMode,
|
|
194
|
+
authManager: this.authManager,
|
|
195
|
+
multiAccount: this.multiAccount,
|
|
196
|
+
accountNames: this.accountNames,
|
|
197
|
+
allowedScopes: this.options.allowedScopes,
|
|
198
|
+
httpMode: this.hidesStdioOnlyTools(),
|
|
199
|
+
userFields: this.options.userFields
|
|
200
|
+
};
|
|
190
201
|
if (this.options.discovery) {
|
|
191
|
-
registerDiscoveryTools(
|
|
192
|
-
server,
|
|
193
|
-
this.graphClient,
|
|
194
|
-
this.options.readOnly,
|
|
195
|
-
this.options.orgMode,
|
|
196
|
-
this.authManager,
|
|
197
|
-
this.multiAccount,
|
|
198
|
-
this.accountNames,
|
|
199
|
-
this.options.enabledTools,
|
|
200
|
-
this.options.allowedScopes,
|
|
201
|
-
this.hidesStdioOnlyTools()
|
|
202
|
-
);
|
|
202
|
+
registerDiscoveryTools(server, this.graphClient, registrationOptions);
|
|
203
203
|
} else {
|
|
204
|
-
registerGraphTools(
|
|
205
|
-
server,
|
|
206
|
-
this.graphClient,
|
|
207
|
-
this.options.readOnly,
|
|
208
|
-
this.options.enabledTools,
|
|
209
|
-
this.options.orgMode,
|
|
210
|
-
this.authManager,
|
|
211
|
-
this.multiAccount,
|
|
212
|
-
this.accountNames,
|
|
213
|
-
this.options.allowedScopes,
|
|
214
|
-
this.hidesStdioOnlyTools()
|
|
215
|
-
);
|
|
204
|
+
registerGraphTools(server, this.graphClient, registrationOptions);
|
|
216
205
|
}
|
|
217
206
|
installToolSchemaRefNormalization(server);
|
|
218
207
|
return server;
|
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.158.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",
|