@softeria/ms-365-mcp-server 0.111.0 → 0.112.2

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/cli.js CHANGED
@@ -47,6 +47,9 @@ program.name("ms-365-mcp-server").description("Microsoft 365 MCP Server").versio
47
47
  ).option(
48
48
  "--obo",
49
49
  "Enable On-Behalf-Of token exchange in HTTP mode. Exchanges the incoming bearer token for a Graph API token using the OBO flow. Requires MS365_MCP_CLIENT_SECRET."
50
+ ).option(
51
+ "--trust-proxy-auth",
52
+ "In HTTP mode, skip the built-in Bearer-token check on /mcp and ignore any forwarded Authorization header. All callers share the locally cached MSAL identity (same path stdio mode uses). Use only when an upstream reverse proxy has already authenticated the caller."
50
53
  ).addOption(
51
54
  // DEPRECATED: kept only so existing deployments that set --base-url or
52
55
  // MS365_MCP_BASE_URL do not crash at startup. Use --public-url /
@@ -149,6 +152,9 @@ function parseArgs() {
149
152
  if (process.env.MS365_MCP_OBO === "true" || process.env.MS365_MCP_OBO === "1") {
150
153
  options.obo = true;
151
154
  }
155
+ if (process.env.MS365_MCP_TRUST_PROXY_AUTH === "true" || process.env.MS365_MCP_TRUST_PROXY_AUTH === "1") {
156
+ options.trustProxyAuth = true;
157
+ }
152
158
  if (options.cloud) {
153
159
  process.env.MS365_MCP_CLOUD_TYPE = options.cloud;
154
160
  }
@@ -0,0 +1,41 @@
1
+ function dumpError(reason, depth = 0) {
2
+ if (depth > 5) {
3
+ return { truncated: true };
4
+ }
5
+ if (reason instanceof Error) {
6
+ const dump = {
7
+ name: reason.name,
8
+ constructor: reason.constructor?.name,
9
+ message: reason.message,
10
+ stack: reason.stack
11
+ };
12
+ const properties = {};
13
+ for (const key of Object.getOwnPropertyNames(reason)) {
14
+ if (key === "name" || key === "message" || key === "stack" || key === "cause") continue;
15
+ properties[key] = reason[key];
16
+ }
17
+ if (Object.keys(properties).length > 0) {
18
+ dump.properties = properties;
19
+ }
20
+ if ("cause" in reason && reason.cause !== void 0) {
21
+ dump.cause = dumpError(reason.cause, depth + 1);
22
+ }
23
+ return dump;
24
+ }
25
+ return { type: typeof reason, value: reason };
26
+ }
27
+ function getActiveResources() {
28
+ const fn = process.getActiveResourcesInfo;
29
+ if (typeof fn !== "function") {
30
+ return "unavailable (node < 17.3)";
31
+ }
32
+ try {
33
+ return fn.call(process);
34
+ } catch (err) {
35
+ return `error: ${err.message}`;
36
+ }
37
+ }
38
+ export {
39
+ dumpError,
40
+ getActiveResources
41
+ };
@@ -381,7 +381,9 @@ const microsoft_graph_chat = z.object({
381
381
  onlineMeetingInfo: microsoft_graph_teamworkOnlineMeetingInfo.optional(),
382
382
  originalCreatedDateTime: z.string().regex(
383
383
  /^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
384
- ).datetime({ offset: true }).nullish(),
384
+ ).datetime({ offset: true }).describe(
385
+ "Timestamp of the original creation time for the chat. The value is null if the chat never entered migration mode."
386
+ ).nullish(),
385
387
  tenantId: z.string().describe("The identifier of the tenant in which the chat was created. Read-only.").nullish(),
386
388
  topic: z.string().describe("(Optional) Subject or topic for the chat. Only available for group chats.").nullish(),
387
389
  viewpoint: microsoft_graph_chatViewpoint.optional(),
@@ -1480,6 +1482,7 @@ const microsoft_graph_group = z.object({
1480
1482
  hideFromOutlookClients: z.boolean().describe(
1481
1483
  "True if the group isn't displayed in Outlook clients, such as Outlook for Windows and Outlook on the web; otherwise, false. The default value is false. Requires $select to retrieve. Supported only on the Get group API (GET /groups/{ID})."
1482
1484
  ).nullish(),
1485
+ infoCatalogs: z.array(z.string()).optional(),
1483
1486
  isArchived: z.boolean().describe(
1484
1487
  "When a group is associated with a team, this property determines whether the team is in read-only mode.To read this property, use the /group/{groupId}/team endpoint or the Get team API. To update this property, use the archiveTeam and unarchiveTeam APIs."
1485
1488
  ).nullish(),
@@ -1504,9 +1507,6 @@ const microsoft_graph_group = z.object({
1504
1507
  ).nullish(),
1505
1508
  membershipRule: z.string().describe(
1506
1509
  "The rule that determines members for this group if the group is a dynamic group (groupTypes contains DynamicMembership). For more information about the syntax of the membership rule, see Membership Rules syntax. Returned by default. Supports $filter (eq, ne, not, ge, le, startsWith)."
1507
- ).nullish(),
1508
- membershipRuleProcessingState: z.string().describe(
1509
- "Indicates whether the dynamic membership processing is on or paused. Possible values are On or Paused. Returned by default. Supports $filter (eq, ne, not, in)."
1510
1510
  ).nullish()
1511
1511
  }).passthrough().passthrough();
1512
1512
  const microsoft_graph_groupCollectionResponse = z.object({
@@ -2759,7 +2759,9 @@ const microsoft_graph_channel = z.lazy(
2759
2759
  migrationMode: microsoft_graph_migrationMode.optional(),
2760
2760
  originalCreatedDateTime: z.string().regex(
2761
2761
  /^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
2762
- ).datetime({ offset: true }).nullish(),
2762
+ ).datetime({ offset: true }).describe(
2763
+ "Timestamp of the original creation time for the channel. The value is null if the channel never entered migration mode."
2764
+ ).nullish(),
2763
2765
  summary: microsoft_graph_channelSummary.optional(),
2764
2766
  tenantId: z.string().describe("The ID of the Microsoft Entra tenant.").nullish(),
2765
2767
  webUrl: z.string().describe(
@@ -2854,7 +2856,7 @@ const microsoft_graph_team = z.lazy(
2854
2856
  ).nullish(),
2855
2857
  allChannels: z.array(microsoft_graph_channel).describe("List of channels either hosted in or shared with the team (incoming channels).").optional(),
2856
2858
  channels: z.array(microsoft_graph_channel).describe("The collection of channels and messages associated with the team.").optional(),
2857
- group: microsoft_graph_group.describe("[Note: Simplified from 74 properties to 25 most common ones]").optional(),
2859
+ group: microsoft_graph_group.describe("[Note: Simplified from 75 properties to 25 most common ones]").optional(),
2858
2860
  incomingChannels: z.array(microsoft_graph_channel).describe("List of channels shared with the team.").optional(),
2859
2861
  installedApps: z.array(microsoft_graph_teamsAppInstallation).describe("The apps installed in this team.").optional(),
2860
2862
  members: z.array(microsoft_graph_conversationMember).describe("Members and owners of the team.").optional(),
@@ -6675,6 +6677,7 @@ You can search within a folder hierarchy, a whole drive, or files shared with th
6675
6677
  hideFromOutlookClients: z.boolean().describe(
6676
6678
  "True if the group isn't displayed in Outlook clients, such as Outlook for Windows and Outlook on the web; otherwise, false. The default value is false. Requires $select to retrieve. Supported only on the Get group API (GET /groups/{ID})."
6677
6679
  ).nullish(),
6680
+ infoCatalogs: z.array(z.string()).optional(),
6678
6681
  isArchived: z.boolean().describe(
6679
6682
  "When a group is associated with a team, this property determines whether the team is in read-only mode.To read this property, use the /group/{groupId}/team endpoint or the Get team API. To update this property, use the archiveTeam and unarchiveTeam APIs."
6680
6683
  ).nullish(),
@@ -6699,9 +6702,6 @@ You can search within a folder hierarchy, a whole drive, or files shared with th
6699
6702
  ).nullish(),
6700
6703
  membershipRule: z.string().describe(
6701
6704
  "The rule that determines members for this group if the group is a dynamic group (groupTypes contains DynamicMembership). For more information about the syntax of the membership rule, see Membership Rules syntax. Returned by default. Supports $filter (eq, ne, not, ge, le, startsWith)."
6702
- ).nullish(),
6703
- membershipRuleProcessingState: z.string().describe(
6704
- "Indicates whether the dynamic membership processing is on or paused. Possible values are On or Paused. Returned by default. Supports $filter (eq, ne, not, in)."
6705
6705
  ).nullish()
6706
6706
  }).passthrough().passthrough()
6707
6707
  }
@@ -6790,6 +6790,7 @@ You can create or update the following types of group: By default, this operatio
6790
6790
  hideFromOutlookClients: z.boolean().describe(
6791
6791
  "True if the group isn't displayed in Outlook clients, such as Outlook for Windows and Outlook on the web; otherwise, false. The default value is false. Requires $select to retrieve. Supported only on the Get group API (GET /groups/{ID})."
6792
6792
  ).nullish(),
6793
+ infoCatalogs: z.array(z.string()).optional(),
6793
6794
  isArchived: z.boolean().describe(
6794
6795
  "When a group is associated with a team, this property determines whether the team is in read-only mode.To read this property, use the /group/{groupId}/team endpoint or the Get team API. To update this property, use the archiveTeam and unarchiveTeam APIs."
6795
6796
  ).nullish(),
@@ -6814,9 +6815,6 @@ You can create or update the following types of group: By default, this operatio
6814
6815
  ).nullish(),
6815
6816
  membershipRule: z.string().describe(
6816
6817
  "The rule that determines members for this group if the group is a dynamic group (groupTypes contains DynamicMembership). For more information about the syntax of the membership rule, see Membership Rules syntax. Returned by default. Supports $filter (eq, ne, not, ge, le, startsWith)."
6817
- ).nullish(),
6818
- membershipRuleProcessingState: z.string().describe(
6819
- "Indicates whether the dynamic membership processing is on or paused. Possible values are On or Paused. Returned by default. Supports $filter (eq, ne, not, in)."
6820
6818
  ).nullish()
6821
6819
  }).passthrough().passthrough()
6822
6820
  }
package/dist/index.js CHANGED
@@ -10,7 +10,27 @@ import {
10
10
  shouldUseLocalAuthStorage
11
11
  } from "./startup-pinning.js";
12
12
  import { createTokenCacheStorage } from "./token-cache-storage.js";
13
+ import { dumpError, getActiveResources } from "./crash-logging.js";
13
14
  import { version } from "./version.js";
15
+ process.on("unhandledRejection", (reason) => {
16
+ const dump = {
17
+ kind: "unhandledRejection",
18
+ reason: dumpError(reason),
19
+ activeResources: getActiveResources()
20
+ };
21
+ console.error("[ms365-mcp] unhandledRejection", JSON.stringify(dump));
22
+ logger.error("unhandledRejection", dump);
23
+ });
24
+ process.on("uncaughtException", (err, origin) => {
25
+ const dump = {
26
+ kind: "uncaughtException",
27
+ origin,
28
+ error: dumpError(err),
29
+ activeResources: getActiveResources()
30
+ };
31
+ console.error("[ms365-mcp] uncaughtException", JSON.stringify(dump));
32
+ logger.error("uncaughtException", dump);
33
+ });
14
34
  async function main() {
15
35
  try {
16
36
  const args = parseArgs();
@@ -17,7 +17,11 @@ function isJwtExpired(token) {
17
17
  return false;
18
18
  }
19
19
  }
20
- const microsoftBearerTokenAuthMiddleware = (req, res, next) => {
20
+ const microsoftBearerTokenAuthMiddleware = (opts = {}) => (req, res, next) => {
21
+ if (opts.trustProxyAuth) {
22
+ next();
23
+ return;
24
+ }
21
25
  const authHeader = req.headers.authorization;
22
26
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
23
27
  res.status(401).set(
@@ -40,6 +44,43 @@ const microsoftBearerTokenAuthMiddleware = (req, res, next) => {
40
44
  req.microsoftAuth = { accessToken };
41
45
  next();
42
46
  };
47
+ class OAuthUpstreamError extends Error {
48
+ constructor(status, raw, body) {
49
+ const suffix = body.error_description ? ` - ${body.error_description}` : "";
50
+ super(`OAuth upstream error: ${body.error}${suffix}`);
51
+ this.name = "OAuthUpstreamError";
52
+ this.status = status;
53
+ this.body = body;
54
+ this.raw = raw;
55
+ }
56
+ }
57
+ function parseUpstreamOAuthError(raw) {
58
+ try {
59
+ const json = JSON.parse(raw);
60
+ if (json !== null && typeof json === "object" && typeof json.error === "string") {
61
+ return json;
62
+ }
63
+ } catch {
64
+ }
65
+ return null;
66
+ }
67
+ function toOAuthErrorResponse(error) {
68
+ if (error instanceof OAuthUpstreamError) {
69
+ const body = {
70
+ error: error.body.error
71
+ };
72
+ if (error.body.error_description) body.error_description = error.body.error_description;
73
+ if (error.body.suberror) body.suberror = error.body.suberror;
74
+ return { status: 400, body };
75
+ }
76
+ return {
77
+ status: 500,
78
+ body: {
79
+ error: "server_error",
80
+ error_description: "Internal server error during token exchange"
81
+ }
82
+ };
83
+ }
43
84
  async function exchangeCodeForToken(code, redirectUri, clientId, clientSecret, tenantId = "common", codeVerifier, cloudType = "global") {
44
85
  const cloudEndpoints = getCloudEndpoints(cloudType);
45
86
  const params = new URLSearchParams({
@@ -62,9 +103,20 @@ async function exchangeCodeForToken(code, redirectUri, clientId, clientSecret, t
62
103
  body: params
63
104
  });
64
105
  if (!response.ok) {
65
- const error = await response.text();
66
- logger.error(`Failed to exchange code for token: ${error}`);
67
- throw new Error(`Failed to exchange code for token: ${error}`);
106
+ const raw = await response.text();
107
+ const parsed = parseUpstreamOAuthError(raw);
108
+ if (parsed) {
109
+ logger.warn(`Token endpoint upstream OAuth error: ${parsed.error}`, {
110
+ status: response.status,
111
+ error: parsed.error,
112
+ suberror: parsed.suberror,
113
+ error_codes: parsed.error_codes,
114
+ correlation_id: parsed.correlation_id
115
+ });
116
+ throw new OAuthUpstreamError(response.status, raw, parsed);
117
+ }
118
+ logger.error(`Failed to exchange code for token: ${raw}`);
119
+ throw new Error(`Failed to exchange code for token: ${raw}`);
68
120
  }
69
121
  return response.json();
70
122
  }
@@ -86,14 +138,27 @@ async function refreshAccessToken(refreshToken, clientId, clientSecret, tenantId
86
138
  body: params
87
139
  });
88
140
  if (!response.ok) {
89
- const error = await response.text();
90
- logger.error(`Failed to refresh token: ${error}`);
91
- throw new Error(`Failed to refresh token: ${error}`);
141
+ const raw = await response.text();
142
+ const parsed = parseUpstreamOAuthError(raw);
143
+ if (parsed) {
144
+ logger.warn(`Token endpoint upstream OAuth error: ${parsed.error}`, {
145
+ status: response.status,
146
+ error: parsed.error,
147
+ suberror: parsed.suberror,
148
+ error_codes: parsed.error_codes,
149
+ correlation_id: parsed.correlation_id
150
+ });
151
+ throw new OAuthUpstreamError(response.status, raw, parsed);
152
+ }
153
+ logger.error(`Failed to refresh token: ${raw}`);
154
+ throw new Error(`Failed to refresh token: ${raw}`);
92
155
  }
93
156
  return response.json();
94
157
  }
95
158
  export {
159
+ OAuthUpstreamError,
96
160
  exchangeCodeForToken,
97
161
  microsoftBearerTokenAuthMiddleware,
98
- refreshAccessToken
162
+ refreshAccessToken,
163
+ toOAuthErrorResponse
99
164
  };
package/dist/server.js CHANGED
@@ -17,12 +17,15 @@ import { MicrosoftOAuthProvider } from "./oauth-provider.js";
17
17
  import {
18
18
  exchangeCodeForToken,
19
19
  microsoftBearerTokenAuthMiddleware,
20
- refreshAccessToken
20
+ OAuthUpstreamError,
21
+ refreshAccessToken,
22
+ toOAuthErrorResponse
21
23
  } from "./lib/microsoft-auth.js";
22
24
  import { isAllowedRedirectUri, parseAllowlist } from "./lib/redirect-uri-validation.js";
23
25
  import { getSecrets } from "./secrets.js";
24
26
  import { getCloudEndpoints } from "./cloud-config.js";
25
27
  import { requestContext } from "./request-context.js";
28
+ import { dumpError } from "./crash-logging.js";
26
29
  import crypto from "node:crypto";
27
30
  import OboClient from "./obo-client.js";
28
31
  function parseHttpOption(httpOption) {
@@ -123,6 +126,11 @@ class MicrosoftGraphServer {
123
126
  "--obo requires MS365_MCP_CLIENT_SECRET to be set (confidential client required for On-Behalf-Of flow)."
124
127
  );
125
128
  }
129
+ if (this.options.trustProxyAuth) {
130
+ throw new Error(
131
+ "--obo cannot be combined with --trust-proxy-auth: the proxy-auth pass-through skips the incoming bearer token that OBO would exchange."
132
+ );
133
+ }
126
134
  this.oboClient = new OboClient(this.secrets);
127
135
  logger.info("On-Behalf-Of (OBO) flow enabled");
128
136
  }
@@ -399,11 +407,18 @@ class MicrosoftGraphServer {
399
407
  });
400
408
  }
401
409
  } catch (error) {
402
- logger.error("Token endpoint error:", error);
403
- res.status(500).json({
404
- error: "server_error",
405
- error_description: "Internal server error during token exchange"
406
- });
410
+ if (error instanceof OAuthUpstreamError) {
411
+ logger.warn("Token endpoint: upstream OAuth error surfaced to client", {
412
+ upstream_status: error.status,
413
+ error: error.body.error,
414
+ suberror: error.body.suberror,
415
+ error_codes: error.body.error_codes
416
+ });
417
+ } else {
418
+ logger.error("Token endpoint error:", error);
419
+ }
420
+ const { status, body } = toOAuthErrorResponse(error);
421
+ res.status(status).json(body);
407
422
  }
408
423
  });
409
424
  app.use(
@@ -412,9 +427,12 @@ class MicrosoftGraphServer {
412
427
  issuerUrl: new URL(publicBase ?? `http://localhost:${port}`)
413
428
  })
414
429
  );
430
+ const mcpAuth = microsoftBearerTokenAuthMiddleware({
431
+ trustProxyAuth: this.options.trustProxyAuth
432
+ });
415
433
  app.get(
416
434
  "/mcp",
417
- microsoftBearerTokenAuthMiddleware,
435
+ mcpAuth,
418
436
  async (req, res) => {
419
437
  const handler = async () => {
420
438
  const server = this.createMcpServer();
@@ -456,7 +474,7 @@ class MicrosoftGraphServer {
456
474
  );
457
475
  app.post(
458
476
  "/mcp",
459
- microsoftBearerTokenAuthMiddleware,
477
+ mcpAuth,
460
478
  async (req, res) => {
461
479
  const handler = async () => {
462
480
  const server = this.createMcpServer();
@@ -520,6 +538,9 @@ class MicrosoftGraphServer {
520
538
  }
521
539
  } else {
522
540
  const transport = new StdioServerTransport();
541
+ transport.onerror = (error) => {
542
+ logger.error("Stdio transport error", { error: dumpError(error) });
543
+ };
523
544
  await this.server.connect(transport);
524
545
  logger.info("Server connected to stdio transport");
525
546
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.111.0",
3
+ "version": "0.112.2",
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",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@azure/msal-node": "^3.8.0",
36
+ "@azure/msal-node": "^5.2.2",
37
37
  "@modelcontextprotocol/sdk": "^1.29.0",
38
38
  "@toon-format/toon": "^0.8.0",
39
39
  "commander": "^11.1.0",
@@ -76,6 +76,6 @@
76
76
  },
77
77
  "repository": {
78
78
  "type": "git",
79
- "url": "https://github.com/softeria/ms-365-mcp-server.git"
79
+ "url": "https://github.com/Softeria/ms-365-mcp-server.git"
80
80
  }
81
81
  }