@softeria/ms-365-mcp-server 0.157.1 → 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.
@@ -1,58 +1,66 @@
1
1
  import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2
2
  import { randomUUID } from "crypto";
3
- import logger from "./logger.js";
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 { isDestructiveOperation } from "./lib/destructive-ops.js";
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
- const allEndpoints = [...api.endpoints, ...betaApi.endpoints];
27
- import { z } from "zod";
28
- import { readFileSync } from "fs";
29
- import { access } from "fs/promises";
30
- import path from "path";
31
- import { fileURLToPath } from "url";
32
- import { TOOL_CATEGORIES } from "./tool-categories.js";
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 { deriveTargetResource } from "./audit-target-resource.js";
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
- TIMEZONE_PARAM_DESCRIPTION,
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
- isSkiptokenApplicable
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([...requestedSelect, ...parseSelectFields(queryParams["$expand"])])
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
- if (mergePages || willProject) {
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, requestedSelect)) {
1551
+ if (!isUserFieldBoundary && !anyFieldPresent(body2, projectionSelect)) {
1446
1552
  logger.warn(
1447
- `None of the requested $select fields (${requestedSelect.join(",")}) appear in the response; returning it untrimmed`
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, readOnly = false, enabledToolsPattern, orgMode = false, authManager, multiAccount = false, accountNames = [], allowedScopesValue, httpMode = false) {
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, readOnly = false, orgMode = false, authManager, multiAccount = false, accountNames = [], enabledTools, allowedScopesValue, httpMode = false) {
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
- return key.toLowerCase() === "id" || key.startsWith("@") || key.startsWith("_");
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 logger, { enableConsoleLogging } from "./logger.js";
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 { MicrosoftOAuthProvider } from "./oauth-provider.js";
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 { loadAttachmentUrlConfig, ATTACHMENT_ROUTE } from "./lib/attachment-url-config.js";
29
- import { AttachmentTicketStore } from "./lib/attachment-tickets.js";
30
- import { configureAttachmentMinting } from "./lib/attachment-minting.js";
31
- import { createAttachmentHandler } from "./attachment-route.js";
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 { hostHeaderValidation } from "@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js";
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;
@@ -529,7 +518,8 @@ class MicrosoftGraphServer {
529
518
  this.options.enabledTools,
530
519
  this.options.readOnly
531
520
  );
532
- const scopeSet = /* @__PURE__ */ new Set([...baseScopes, "User.Read", "offline_access"]);
521
+ const oboExtraScopes = this.options.obo ? parseAllowedScopes(this.options.extraScopes) ?? [] : [];
522
+ const scopeSet = /* @__PURE__ */ new Set([...baseScopes, ...oboExtraScopes, "User.Read", "offline_access"]);
533
523
  microsoftAuthUrl.searchParams.set("scope", Array.from(scopeSet).join(" "));
534
524
  res.redirect(microsoftAuthUrl.toString());
535
525
  });
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.157.1",
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",