@softeria/ms-365-mcp-server 0.158.2 → 0.159.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/README.md CHANGED
@@ -16,6 +16,8 @@ This server supports multiple Microsoft cloud environments:
16
16
  | **Global** (default) | International Microsoft 365 | login.microsoftonline.com | graph.microsoft.com |
17
17
  | **China** (21Vianet) | Microsoft 365 operated by 21Vianet | login.chinacloudapi.cn | microsoftgraph.chinacloudapi.cn |
18
18
 
19
+ To route Graph traffic through a proxy of your own (one that terminates the client's plain HTTP and originates TLS to Microsoft), set `MS365_MCP_GRAPH_BASE_URL` to its base URL; see the environment variable list under CLI Options, including what still bypasses it.
20
+
19
21
  ## Prerequisites
20
22
 
21
23
  - Node.js >= 20 (recommended)
@@ -180,7 +182,7 @@ npx @softeria/ms-365-mcp-server \
180
182
  --allowed-scopes 'User.Read Files.Read Notes.Read Tasks.Read Sites.Selected'
181
183
  ```
182
184
 
183
- In HTTP mode, OAuth discovery advertises the effective filtered permissions so clients request the same consent surface. On-Behalf-Of mode (`--obo`) still advertises `api://<clientId>/access_as_user` for protected-resource metadata; `--allowed-scopes` does not override OBO.
185
+ In HTTP mode, OAuth discovery advertises the effective filtered permissions so clients request the same consent surface. On-Behalf-Of mode (`--obo`) still advertises `<clientId>/access_as_user` for protected-resource metadata and requests it on `/authorize`; `--allowed-scopes` does not override OBO, it only narrows the tool surface.
184
186
 
185
187
  ### Restricting user profile fields
186
188
 
@@ -734,6 +736,7 @@ Environment variables:
734
736
  - `MS365_MCP_ATTACHMENT_HOST=<host>`: Interface the `MS365_MCP_ATTACHMENT_PORT` listener binds (alternative to --attachment-host; requires `--attachment-port`). Defaults to the host `--http` bound — which for a wildcard `--http` means both ports answer everywhere and the port split isolates nothing. See "Splitting the attachment listener"
735
737
  - `MS365_MCP_HTTP_LOCAL_FILE_TOOLS=true|1`: Register download-bytes-to-file over HTTP (alternative to --http-local-file-tools; same restrictions)
736
738
  - `MS365_MCP_CLOUD_TYPE=global|china`: Microsoft cloud environment (alternative to --cloud flag)
739
+ - `MS365_MCP_GRAPH_BASE_URL=<url>`: Send Graph API requests to this base URL instead of the cloud's `graph.microsoft.com` (e.g. `http://127.0.0.1:10255/tenant-a/graph` for an egress proxy that originates TLS itself). Absolute http(s) URL without query or fragment; a path prefix is kept, so requests go to `<url>/v1.0/...`. The login authority and the On-Behalf-Of token resource are unchanged. Two things bypass it: `get-download-url` returns Graph's pre-authenticated SharePoint URL, and the `302` from `/content` points straight at SharePoint, so whoever follows either connects to SharePoint directly; if you control egress, allow those SharePoint hosts or disable those tools. Not read from `.env`
737
740
  - `LOG_LEVEL`: Set logging level (default: 'info')
738
741
  - `SILENT=true|1`: Disable console output
739
742
  - `MS365_MCP_REDACT_PII=false|0`: Disable scrubbing of JWTs, Bearer headers, OAuth token fields, and email addresses from log messages (default: enabled). The server handles live Graph bearer tokens, so redaction is on unless you opt out for fully verbose local debugging.
package/dist/auth.js CHANGED
@@ -4,7 +4,7 @@ import { readFileSync } from "fs";
4
4
  import { fileURLToPath } from "url";
5
5
  import path from "path";
6
6
  import { getSecrets } from "./secrets.js";
7
- import { getCloudEndpoints, getDefaultClientId } from "./cloud-config.js";
7
+ import { getCloudEndpoints, getDefaultClientId, getGraphBaseUrl } from "./cloud-config.js";
8
8
  import {
9
9
  dedupeRefreshTokens
10
10
  } from "./lib/cache-dedupe.js";
@@ -910,8 +910,7 @@ class AuthManager {
910
910
  logger.info("Token retrieved successfully, testing Graph API access...");
911
911
  try {
912
912
  const secrets = await getSecrets();
913
- const cloudEndpoints = getCloudEndpoints(secrets.cloudType);
914
- const response = await fetch(`${cloudEndpoints.graphApi}/v1.0/me`, {
913
+ const response = await fetch(`${getGraphBaseUrl(secrets.cloudType)}/v1.0/me`, {
915
914
  headers: {
916
915
  Authorization: `Bearer ${token}`
917
916
  }
@@ -17,6 +17,23 @@ const DEFAULT_CLIENT_IDS = {
17
17
  function getDefaultClientId(cloudType = "global") {
18
18
  return DEFAULT_CLIENT_IDS[cloudType];
19
19
  }
20
+ const GRAPH_BASE_URL_ENV = "MS365_MCP_GRAPH_BASE_URL";
21
+ function getGraphBaseUrlOverride() {
22
+ const raw = process.env[GRAPH_BASE_URL_ENV]?.trim();
23
+ if (!raw) return void 0;
24
+ let parsed;
25
+ try {
26
+ parsed = new URL(raw);
27
+ } catch {
28
+ parsed = void 0;
29
+ }
30
+ if (!parsed || !/^https?:$/.test(parsed.protocol) || parsed.search || parsed.hash) {
31
+ throw new Error(
32
+ `${GRAPH_BASE_URL_ENV} must be an absolute http(s) URL without query or fragment, got: ${raw}`
33
+ );
34
+ }
35
+ return raw.replace(/\/+$/, "");
36
+ }
20
37
  function getCloudEndpoints(cloudType = "global") {
21
38
  const endpoints = CLOUD_ENDPOINTS[cloudType];
22
39
  if (!endpoints) {
@@ -26,6 +43,9 @@ function getCloudEndpoints(cloudType = "global") {
26
43
  }
27
44
  return endpoints;
28
45
  }
46
+ function getGraphBaseUrl(cloudType = "global") {
47
+ return getGraphBaseUrlOverride() ?? getCloudEndpoints(cloudType).graphApi;
48
+ }
29
49
  function isValidCloudType(value) {
30
50
  return value in CLOUD_ENDPOINTS;
31
51
  }
@@ -44,6 +64,7 @@ export {
44
64
  DEFAULT_CLIENT_IDS,
45
65
  getCloudEndpoints,
46
66
  getDefaultClientId,
67
+ getGraphBaseUrl,
47
68
  isValidCloudType,
48
69
  parseCloudType
49
70
  };
@@ -1,6 +1,6 @@
1
1
  import logger from "./logger.js";
2
2
  import { encode as toonEncode } from "@toon-format/toon";
3
- import { getCloudEndpoints } from "./cloud-config.js";
3
+ import { getGraphBaseUrl } from "./cloud-config.js";
4
4
  import { getRequestTokens } from "./request-context.js";
5
5
  import {
6
6
  fetchWithResilience,
@@ -293,9 +293,8 @@ class GraphClient {
293
293
  );
294
294
  }
295
295
  async performRequest(endpoint, accessToken, options) {
296
- const cloudEndpoints = getCloudEndpoints(this.secrets.cloudType);
297
296
  const apiVersion = options.apiVersion || "v1.0";
298
- const url = `${cloudEndpoints.graphApi}/${apiVersion}${endpoint}`;
297
+ const url = `${getGraphBaseUrl(this.secrets.cloudType)}/${apiVersion}${endpoint}`;
299
298
  logger.info(`[GRAPH CLIENT] Final URL being sent to Microsoft: ${url}`);
300
299
  const method = options.method || "GET";
301
300
  const signedBody = applyMessageSignoffToRequest(method, endpoint, options.body);
@@ -1,6 +1,6 @@
1
1
  import { ProxyOAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js";
2
2
  import logger from "./logger.js";
3
- import { getCloudEndpoints } from "./cloud-config.js";
3
+ import { getCloudEndpoints, getGraphBaseUrl } from "./cloud-config.js";
4
4
  class MicrosoftOAuthProvider extends ProxyOAuthServerProvider {
5
5
  constructor(authManager, secrets) {
6
6
  const tenantId = secrets.tenantId || "common";
@@ -14,7 +14,7 @@ class MicrosoftOAuthProvider extends ProxyOAuthServerProvider {
14
14
  },
15
15
  verifyAccessToken: async (token) => {
16
16
  try {
17
- const response = await fetch(`${cloudEndpoints.graphApi}/v1.0/me`, {
17
+ const response = await fetch(`${getGraphBaseUrl(secrets.cloudType)}/v1.0/me`, {
18
18
  headers: {
19
19
  Authorization: `Bearer ${token}`
20
20
  }
package/dist/server.js CHANGED
@@ -513,7 +513,8 @@ class MicrosoftGraphServer {
513
513
  microsoftAuthUrl.searchParams.set("client_id", clientId);
514
514
  const explicitAllowedScopes = parseAllowedScopes(this.options.allowedScopes);
515
515
  const clientScope = microsoftAuthUrl.searchParams.get("scope");
516
- const baseScopes = explicitAllowedScopes !== void 0 ? resolveAuthScopes(this.options) : clientScope ? clientScope.split(/\s+/).filter(Boolean) : buildScopesFromEndpoints(
516
+ const clientScopes = (clientScope ?? "").split(/\s+/).filter(Boolean);
517
+ const baseScopes = this.options.obo ? clientScopes.some((scope) => scope.endsWith("/access_as_user")) ? clientScopes : [`${clientId}/access_as_user`, ...clientScopes] : explicitAllowedScopes !== void 0 ? resolveAuthScopes(this.options) : clientScope ? clientScopes : buildScopesFromEndpoints(
517
518
  this.options.orgMode,
518
519
  this.options.enabledTools,
519
520
  this.options.readOnly
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.158.2",
4
+ "version": "0.159.1",
5
5
  "description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",