@softeria/ms-365-mcp-server 0.123.0 → 0.124.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.
package/dist/auth.js CHANGED
@@ -49,13 +49,47 @@ function getEndpointRequiredScopes(endpoint, includeWorkAccountScopes = false) {
49
49
  return [];
50
50
  }
51
51
  const scopes = /* @__PURE__ */ new Set();
52
- if (endpoint.scopes && Array.isArray(endpoint.scopes)) {
53
- endpoint.scopes.forEach((scope) => scopes.add(scope));
52
+ getEndpointScopeGroups(endpoint, includeWorkAccountScopes).forEach(
53
+ (group) => group.forEach((scope) => scopes.add(scope))
54
+ );
55
+ return Array.from(scopes);
56
+ }
57
+ function toScopeGroups(value) {
58
+ if (!value || value.length === 0) {
59
+ return [];
54
60
  }
55
- if (includeWorkAccountScopes && endpoint.workScopes && Array.isArray(endpoint.workScopes)) {
56
- endpoint.workScopes.forEach((scope) => scopes.add(scope));
61
+ return Array.isArray(value[0]) ? value : [value];
62
+ }
63
+ function getEndpointScopeGroups(endpoint, includeWorkAccountScopes = false) {
64
+ if (!endpoint) {
65
+ return [];
57
66
  }
58
- return Array.from(scopes);
67
+ const groups = [...toScopeGroups(endpoint.scopes)];
68
+ if (includeWorkAccountScopes) {
69
+ groups.push(...toScopeGroups(endpoint.workScopes));
70
+ }
71
+ return groups;
72
+ }
73
+ function getEndpointLoginScopes(endpoint, includeWorkAccountScopes = false) {
74
+ const groups = getEndpointScopeGroups(endpoint, includeWorkAccountScopes);
75
+ return groups.length > 0 ? groups[0] : [];
76
+ }
77
+ function getMissingAllowedScopesForGroups(scopeGroups, allowedScopes) {
78
+ if (allowedScopes === void 0 || scopeGroups.length === 0) {
79
+ return [];
80
+ }
81
+ const coveredAllowedScopes = new Set(collapseScopeHierarchy(allowedScopes));
82
+ let closest;
83
+ for (const group of scopeGroups) {
84
+ const missing = group.filter((scope) => !coveredAllowedScopes.has(scope));
85
+ if (missing.length === 0) {
86
+ return [];
87
+ }
88
+ if (!closest || missing.length < closest.length) {
89
+ closest = missing;
90
+ }
91
+ }
92
+ return closest ?? [];
59
93
  }
60
94
  function collapseRedundantScopes(scopes) {
61
95
  const scopesSet = new Set(scopes);
@@ -91,7 +125,7 @@ function buildScopesFromEndpoints(includeWorkAccountScopes = false, enabledTools
91
125
  if (!includeWorkAccountScopes && !endpoint.scopes && endpoint.workScopes) {
92
126
  return;
93
127
  }
94
- getEndpointRequiredScopes(endpoint, includeWorkAccountScopes).forEach(
128
+ getEndpointLoginScopes(endpoint, includeWorkAccountScopes).forEach(
95
129
  (scope) => scopesSet.add(scope)
96
130
  );
97
131
  });
@@ -173,6 +207,7 @@ function buildAllowedScopeDiagnostics(options = {}) {
173
207
  }
174
208
  const normalToolScopes = /* @__PURE__ */ new Set();
175
209
  const effectiveToolScopes = /* @__PURE__ */ new Set();
210
+ const effectiveToolScopesAllGroups = /* @__PURE__ */ new Set();
176
211
  const disabledTools = [];
177
212
  for (const endpoint of endpoints.default) {
178
213
  if (!endpointMatchesNormalToolSurface(
@@ -183,18 +218,21 @@ function buildAllowedScopeDiagnostics(options = {}) {
183
218
  )) {
184
219
  continue;
185
220
  }
186
- const requiredScopes = getEndpointRequiredScopes(endpoint, Boolean(options.orgMode));
187
- requiredScopes.forEach((scope) => normalToolScopes.add(scope));
188
- const missingScopes = getMissingAllowedScopes(requiredScopes, allowedScopes);
221
+ const scopeGroups = getEndpointScopeGroups(endpoint, Boolean(options.orgMode));
222
+ const loginScopes = getEndpointLoginScopes(endpoint, Boolean(options.orgMode));
223
+ const allScopes = getEndpointRequiredScopes(endpoint, Boolean(options.orgMode));
224
+ loginScopes.forEach((scope) => normalToolScopes.add(scope));
225
+ const missingScopes = getMissingAllowedScopesForGroups(scopeGroups, allowedScopes);
189
226
  if (missingScopes.length > 0) {
190
227
  disabledTools.push({
191
228
  toolName: endpoint.toolName,
192
- requiredScopes: requiredScopes.sort((a, b) => a.localeCompare(b)),
229
+ requiredScopes: allScopes.sort((a, b) => a.localeCompare(b)),
193
230
  missingScopes: missingScopes.sort((a, b) => a.localeCompare(b))
194
231
  });
195
232
  continue;
196
233
  }
197
- requiredScopes.forEach((scope) => effectiveToolScopes.add(scope));
234
+ loginScopes.forEach((scope) => effectiveToolScopes.add(scope));
235
+ allScopes.forEach((scope) => effectiveToolScopesAllGroups.add(scope));
198
236
  }
199
237
  const toolPermissions = collapseRedundantScopes(Array.from(normalToolScopes)).sort(
200
238
  (a, b) => a.localeCompare(b)
@@ -206,7 +244,8 @@ function buildAllowedScopeDiagnostics(options = {}) {
206
244
  const missingAllowedScopesForTools = Array.from(
207
245
  new Set(disabledTools.flatMap((tool) => tool.missingScopes))
208
246
  ).sort((a, b) => a.localeCompare(b));
209
- const extraAllowedScopesNotUsedByTools = sortedAllowedScopes?.filter((scope) => !isScopeUsedByTools(scope, effectivePermissions)) ?? [];
247
+ const allEffectiveToolScopes = Array.from(effectiveToolScopesAllGroups);
248
+ const extraAllowedScopesNotUsedByTools = sortedAllowedScopes?.filter((scope) => !isScopeUsedByTools(scope, allEffectiveToolScopes)) ?? [];
210
249
  return {
211
250
  permissions: effectivePermissions,
212
251
  toolPermissions,
@@ -811,7 +850,9 @@ export {
811
850
  auth_default as default,
812
851
  describeAuthError,
813
852
  getEndpointRequiredScopes,
853
+ getEndpointScopeGroups,
814
854
  getMissingAllowedScopes,
855
+ getMissingAllowedScopesForGroups,
815
856
  getSelectedAccountPath,
816
857
  getTokenCachePath,
817
858
  parseAllowedScopes,
@@ -1748,7 +1748,7 @@
1748
1748
  "toolName": "copilot-retrieve",
1749
1749
  "presets": ["search", "work"],
1750
1750
  "readOnly": true,
1751
- "workScopes": ["Files.Read.All", "Sites.Read.All", "ExternalItem.Read.All"],
1751
+ "workScopes": [["Files.Read.All", "Sites.Read.All"], ["ExternalItem.Read.All"]],
1752
1752
  "requestBodySchema": {
1753
1753
  "type": "object",
1754
1754
  "required": ["queryString", "dataSource"],
@@ -1782,7 +1782,7 @@
1782
1782
  }
1783
1783
  }
1784
1784
  },
1785
- "llmTip": "Semantic/hybrid search over Microsoft 365 content via the Copilot Retrieval API. Grounds a natural-language query against the same hybrid index that powers Microsoft 365 Copilot and returns permission-trimmed text extracts with their source URLs — unlike search-query/search-onedrive-files/search-sharepoint-sites, which are lexical KQL. Prefer this for paraphrase/intent queries (e.g. resolving an informal project nickname, or matching 'packaging requirements' against text that never uses that phrase). Pass the request fields nested under a 'body' object: { queryString, dataSource, filterExpression?, resourceMetadata?, maximumNumberOfResults? }. Returns { retrievalHits: [{ webUrl, extracts: [{ text, relevanceScore }], resourceType, resourceMetadata, sensitivityLabel? }] }. Work/school accounts only (personal accounts are not supported); needs delegated Files.Read.All + Sites.Read.All (SharePoint/OneDrive) or ExternalItem.Read.All (Copilot connectors)."
1785
+ "llmTip": "Semantic/hybrid search over Microsoft 365 content via the Copilot Retrieval API. Grounds a natural-language query against the same hybrid index that powers Microsoft 365 Copilot and returns permission-trimmed text extracts with their source URLs — unlike search-query/search-onedrive-files/search-sharepoint-sites, which are lexical KQL. Prefer this for paraphrase/intent queries (e.g. resolving an informal project nickname, or matching 'packaging requirements' against text that never uses that phrase). Pass the request fields nested under a 'body' object: { queryString, dataSource, filterExpression?, resourceMetadata?, maximumNumberOfResults? }. Returns { retrievalHits: [{ webUrl, extracts: [{ text, relevanceScore }], resourceType, resourceMetadata, sensitivityLabel? }] }. Work/school accounts only (personal accounts are not supported). dataSource 'sharePoint'/'oneDriveBusiness' need delegated Files.Read.All + Sites.Read.All (requested by default); dataSource 'externalItem' (Copilot connectors) needs ExternalItem.Read.All, which is not requested by default - add it with --extra-scopes ExternalItem.Read.All."
1786
1786
  },
1787
1787
  {
1788
1788
  "pathPattern": "/me/onlineMeetings",
@@ -2,8 +2,8 @@ import { randomUUID } from "crypto";
2
2
  import logger from "./logger.js";
3
3
  import { auditLog, getUserIdentityForAudit } from "./audit-log.js";
4
4
  import {
5
- getEndpointRequiredScopes,
6
- getMissingAllowedScopes,
5
+ getEndpointScopeGroups,
6
+ getMissingAllowedScopesForGroups,
7
7
  parseAllowedScopes
8
8
  } from "./auth.js";
9
9
  import { api } from "./generated/client.js";
@@ -294,6 +294,9 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
294
294
  const encodedValue = encodeURIComponent(paramValue).replace(/%3D/g, "=");
295
295
  path2 = path2.replace(`{${paramName}}`, encodedValue).replace(`:${paramName}`, encodedValue).replace(`{${camelCaseParamName}}`, encodedValue).replace(`:${camelCaseParamName}`, encodedValue);
296
296
  logger.info(`Path param fallback: replaced :${camelCaseParamName} with encoded value`);
297
+ } else if (isOdataParam) {
298
+ queryParams[fixedParamName] = `${paramValue}`;
299
+ logger.info(`OData param fallback: forwarded ${fixedParamName}=${paramValue}`);
297
300
  }
298
301
  }
299
302
  clampTopQueryParam(queryParams);
@@ -522,8 +525,10 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
522
525
  skippedCount++;
523
526
  continue;
524
527
  }
525
- const requiredScopes = getEndpointRequiredScopes(endpointConfig, orgMode);
526
- const missingScopes = allowedScopes !== void 0 && !endpointConfig ? ["endpoint scope metadata"] : getMissingAllowedScopes(requiredScopes, allowedScopes);
528
+ const missingScopes = allowedScopes !== void 0 && !endpointConfig ? ["endpoint scope metadata"] : getMissingAllowedScopesForGroups(
529
+ getEndpointScopeGroups(endpointConfig, orgMode),
530
+ allowedScopes
531
+ );
527
532
  if (missingScopes.length > 0) {
528
533
  disabledByAllowedScopes.push({ toolName: tool.alias, missingScopes });
529
534
  skippedCount++;
@@ -674,8 +679,8 @@ function buildToolsRegistry(readOnly, orgMode, enabledToolsRegex, allowedScopesV
674
679
  if (enabledToolsRegex && !enabledToolsRegex.test(tool.alias)) {
675
680
  continue;
676
681
  }
677
- const missingScopes = allowedScopes !== void 0 && !endpointConfig ? ["endpoint scope metadata"] : getMissingAllowedScopes(
678
- getEndpointRequiredScopes(endpointConfig, orgMode),
682
+ const missingScopes = allowedScopes !== void 0 && !endpointConfig ? ["endpoint scope metadata"] : getMissingAllowedScopesForGroups(
683
+ getEndpointScopeGroups(endpointConfig, orgMode),
679
684
  allowedScopes
680
685
  );
681
686
  if (missingScopes.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.123.0",
3
+ "version": "0.124.1",
4
4
  "description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1748,7 +1748,7 @@
1748
1748
  "toolName": "copilot-retrieve",
1749
1749
  "presets": ["search", "work"],
1750
1750
  "readOnly": true,
1751
- "workScopes": ["Files.Read.All", "Sites.Read.All", "ExternalItem.Read.All"],
1751
+ "workScopes": [["Files.Read.All", "Sites.Read.All"], ["ExternalItem.Read.All"]],
1752
1752
  "requestBodySchema": {
1753
1753
  "type": "object",
1754
1754
  "required": ["queryString", "dataSource"],
@@ -1782,7 +1782,7 @@
1782
1782
  }
1783
1783
  }
1784
1784
  },
1785
- "llmTip": "Semantic/hybrid search over Microsoft 365 content via the Copilot Retrieval API. Grounds a natural-language query against the same hybrid index that powers Microsoft 365 Copilot and returns permission-trimmed text extracts with their source URLs — unlike search-query/search-onedrive-files/search-sharepoint-sites, which are lexical KQL. Prefer this for paraphrase/intent queries (e.g. resolving an informal project nickname, or matching 'packaging requirements' against text that never uses that phrase). Pass the request fields nested under a 'body' object: { queryString, dataSource, filterExpression?, resourceMetadata?, maximumNumberOfResults? }. Returns { retrievalHits: [{ webUrl, extracts: [{ text, relevanceScore }], resourceType, resourceMetadata, sensitivityLabel? }] }. Work/school accounts only (personal accounts are not supported); needs delegated Files.Read.All + Sites.Read.All (SharePoint/OneDrive) or ExternalItem.Read.All (Copilot connectors)."
1785
+ "llmTip": "Semantic/hybrid search over Microsoft 365 content via the Copilot Retrieval API. Grounds a natural-language query against the same hybrid index that powers Microsoft 365 Copilot and returns permission-trimmed text extracts with their source URLs — unlike search-query/search-onedrive-files/search-sharepoint-sites, which are lexical KQL. Prefer this for paraphrase/intent queries (e.g. resolving an informal project nickname, or matching 'packaging requirements' against text that never uses that phrase). Pass the request fields nested under a 'body' object: { queryString, dataSource, filterExpression?, resourceMetadata?, maximumNumberOfResults? }. Returns { retrievalHits: [{ webUrl, extracts: [{ text, relevanceScore }], resourceType, resourceMetadata, sensitivityLabel? }] }. Work/school accounts only (personal accounts are not supported). dataSource 'sharePoint'/'oneDriveBusiness' need delegated Files.Read.All + Sites.Read.All (requested by default); dataSource 'externalItem' (Copilot connectors) needs ExternalItem.Read.All, which is not requested by default - add it with --extra-scopes ExternalItem.Read.All."
1786
1786
  },
1787
1787
  {
1788
1788
  "pathPattern": "/me/onlineMeetings",