@softeria/ms-365-mcp-server 0.106.2 → 0.107.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.
@@ -106,6 +106,27 @@
106
106
  "toolName": "create-shared-mailbox-draft",
107
107
  "workScopes": ["Mail.ReadWrite.Shared"]
108
108
  },
109
+ {
110
+ "pathPattern": "/users/{user-id}/messages/{message-id}/reply",
111
+ "method": "post",
112
+ "toolName": "reply-shared-mailbox-mail",
113
+ "workScopes": ["Mail.Send.Shared"],
114
+ "llmTip": "Reply to a message from a shared mailbox preserving full HTML formatting and conversation thread. The 'user-id' is the shared mailbox email address (e.g. support@contoso.com). The 'comment' field is your reply text. Do NOT reconstruct the email manually. Requires Send As permission on the shared mailbox in Exchange."
115
+ },
116
+ {
117
+ "pathPattern": "/users/{user-id}/messages/{message-id}/replyAll",
118
+ "method": "post",
119
+ "toolName": "reply-all-shared-mailbox-mail",
120
+ "workScopes": ["Mail.Send.Shared"],
121
+ "llmTip": "Reply-all to a message from a shared mailbox preserving full HTML formatting and conversation thread. The 'user-id' is the shared mailbox email address. The 'comment' field is your reply text. Requires Send As permission on the shared mailbox in Exchange."
122
+ },
123
+ {
124
+ "pathPattern": "/users/{user-id}/messages/{message-id}/forward",
125
+ "method": "post",
126
+ "toolName": "forward-shared-mailbox-mail",
127
+ "workScopes": ["Mail.Send.Shared"],
128
+ "llmTip": "Forward a message from a shared mailbox preserving full HTML formatting and attachments. The 'user-id' is the shared mailbox email address. 'toRecipients' is required. The 'comment' field adds text above the forwarded content. Requires Send As permission on the shared mailbox in Exchange."
129
+ },
109
130
  {
110
131
  "pathPattern": "/users",
111
132
  "method": "get",
@@ -14465,6 +14465,66 @@ To monitor future changes, call the delta API by using the @odata.deltaLink in t
14465
14465
  ],
14466
14466
  response: z.void()
14467
14467
  },
14468
+ {
14469
+ method: "post",
14470
+ path: "/users/:userId/messages/:messageId/forward",
14471
+ alias: "forward-shared-mailbox-mail",
14472
+ description: `Forward a message using either JSON or MIME format. When using JSON format, you can:
14473
+ - Specify either a comment or the body property of the message parameter. Specifying both will return an HTTP 400 Bad Request error.
14474
+ - Specify either the toRecipients parameter or the toRecipients property of the message parameter. Specifying both or specifying neither will return an HTTP 400 Bad Request error. When using MIME format:
14475
+ - Provide the applicable Internet message headers and the MIME content, all encoded in base64 format in the request body.
14476
+ - Add any attachments and S/MIME properties to the MIME content. This method saves the message in the Sent Items folder. Alternatively, create a draft to forward a message, and send it later.`,
14477
+ requestFormat: "json",
14478
+ parameters: [
14479
+ {
14480
+ name: "body",
14481
+ description: `Action parameters`,
14482
+ type: "Body",
14483
+ schema: create_forward_draft_Body
14484
+ }
14485
+ ],
14486
+ response: z.void()
14487
+ },
14488
+ {
14489
+ method: "post",
14490
+ path: "/users/:userId/messages/:messageId/reply",
14491
+ alias: "reply-shared-mailbox-mail",
14492
+ description: `Reply to the sender of a message using either JSON or MIME format. When using JSON format:
14493
+ * Specify either a comment or the body property of the message parameter. Specifying both will return an HTTP 400 Bad Request error.
14494
+ * If the original message specifies a recipient in the replyTo property, per Internet Message Format (RFC 2822), send the reply to the recipients in replyTo and not the recipient in the from property. When using MIME format:
14495
+ - Provide the applicable Internet message headers and the MIME content, all encoded in base64 format in the request body.
14496
+ - Add any attachments and S/MIME properties to the MIME content. This method saves the message in the Sent Items folder. Alternatively, create a draft to reply to an existing message and send it later.`,
14497
+ requestFormat: "json",
14498
+ parameters: [
14499
+ {
14500
+ name: "body",
14501
+ description: `Action parameters`,
14502
+ type: "Body",
14503
+ schema: create_reply_draft_Body
14504
+ }
14505
+ ],
14506
+ response: z.void()
14507
+ },
14508
+ {
14509
+ method: "post",
14510
+ path: "/users/:userId/messages/:messageId/replyAll",
14511
+ alias: "reply-all-shared-mailbox-mail",
14512
+ description: `Reply to all recipients of a message using either JSON or MIME format. When using JSON format:
14513
+ - Specify either a comment or the body property of the message parameter. Specifying both will return an HTTP 400 Bad Request error.
14514
+ - If the original message specifies a recipient in the replyTo property, per Internet Message Format (RFC 2822), send the reply to the recipients in replyTo and not the recipient in the from property. When using MIME format:
14515
+ - Provide the applicable Internet message headers and the MIME content, all encoded in base64 format in the request body.
14516
+ - Add any attachments and S/MIME properties to the MIME content. This method saves the message in the Sent Items folder. Alternatively, create a draft to reply-all to a message and send it later.`,
14517
+ requestFormat: "json",
14518
+ parameters: [
14519
+ {
14520
+ name: "body",
14521
+ description: `Action parameters`,
14522
+ type: "Body",
14523
+ schema: create_reply_draft_Body
14524
+ }
14525
+ ],
14526
+ response: z.void()
14527
+ },
14468
14528
  {
14469
14529
  method: "get",
14470
14530
  path: "/users/:userId/presence",
@@ -0,0 +1,33 @@
1
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
2
+ function isLoopbackHost(host) {
3
+ return LOOPBACK_HOSTS.has(host.toLowerCase());
4
+ }
5
+ function parseAllowlist(raw) {
6
+ if (!raw) return null;
7
+ const list = raw.split(",").map((s) => s.trim()).filter(Boolean);
8
+ return list.length > 0 ? list : null;
9
+ }
10
+ function isAllowedRedirectUri(value, allowlist) {
11
+ if (typeof value !== "string" || value.length === 0) return false;
12
+ let url;
13
+ try {
14
+ url = new URL(value);
15
+ } catch {
16
+ return false;
17
+ }
18
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
19
+ return false;
20
+ }
21
+ if (allowlist && allowlist.length > 0) {
22
+ return allowlist.includes(value);
23
+ }
24
+ if (url.protocol === "http:") {
25
+ return isLoopbackHost(url.hostname);
26
+ }
27
+ return true;
28
+ }
29
+ export {
30
+ isAllowedRedirectUri,
31
+ isLoopbackHost,
32
+ parseAllowlist
33
+ };
package/dist/server.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  microsoftBearerTokenAuthMiddleware,
16
16
  refreshAccessToken
17
17
  } from "./lib/microsoft-auth.js";
18
+ import { isAllowedRedirectUri, parseAllowlist } from "./lib/redirect-uri-validation.js";
18
19
  import { getSecrets } from "./secrets.js";
19
20
  import { getCloudEndpoints } from "./cloud-config.js";
20
21
  import { requestContext } from "./request-context.js";
@@ -226,6 +227,20 @@ class MicrosoftGraphServer {
226
227
  const clientCodeChallenge = url.searchParams.get("code_challenge");
227
228
  const clientCodeChallengeMethod = url.searchParams.get("code_challenge_method");
228
229
  const state = url.searchParams.get("state");
230
+ const redirectUriParam = url.searchParams.get("redirect_uri");
231
+ if (redirectUriParam) {
232
+ const allowlist = parseAllowlist(process.env.MS365_MCP_ALLOWED_REDIRECT_URIS);
233
+ if (!isAllowedRedirectUri(redirectUriParam, allowlist)) {
234
+ logger.warn("Rejected /authorize request with disallowed redirect_uri", {
235
+ redirect_uri: redirectUriParam
236
+ });
237
+ res.status(400).json({
238
+ error: "invalid_request",
239
+ error_description: "redirect_uri is not allowed"
240
+ });
241
+ return;
242
+ }
243
+ }
229
244
  const allowedParams = [
230
245
  "response_type",
231
246
  "redirect_uri",
@@ -130,6 +130,28 @@ When deploying for an organization, create a dedicated app registration instead
130
130
 
131
131
  5. **Store credentials** in Key Vault (see [Azure Key Vault Integration](../README.md#azure-key-vault-integration))
132
132
 
133
+ ## Redirect URI Validation
134
+
135
+ The /authorize endpoint defensively validates client-supplied `redirect_uri` values before forwarding them to Microsoft Entra (CWE-601, Open Redirect). Microsoft Entra also validates the URI against your app registration, but this server-side check rejects obviously dangerous schemes (`javascript:`, `data:`, `file:`, …) and arbitrary remote `http://` origins before the request leaves the server.
136
+
137
+ Default behaviour (no explicit allowlist):
138
+
139
+ - Only `http:` and `https:` schemes are accepted.
140
+ - `http:` is only allowed for loopback hosts (`localhost`, `127.0.0.1`, `::1`).
141
+ - All other `https://` origins are accepted (Entra still has the final say).
142
+
143
+ For production deployments, configure an explicit allowlist via the `MS365_MCP_ALLOWED_REDIRECT_URIS` environment variable. It takes a comma-separated list of exact URIs; only exact string matches pass validation:
144
+
145
+ ```bash
146
+ # Single redirect URI
147
+ MS365_MCP_ALLOWED_REDIRECT_URIS=https://mcp.example.com/auth/callback
148
+
149
+ # Multiple URIs (comma-separated, no spaces required)
150
+ MS365_MCP_ALLOWED_REDIRECT_URIS=https://mcp.example.com/auth/callback,https://staging.example.com/auth/callback
151
+ ```
152
+
153
+ The list should mirror the redirect URIs registered on your Azure AD app registration. Leaving the variable unset falls back to the default behaviour above, which is appropriate for local development but not recommended for shared/production deployments.
154
+
133
155
  ## Reverse Proxy / Custom Domain
134
156
 
135
157
  When running behind a reverse proxy, set `MS365_MCP_PUBLIC_URL` so that the OAuth authorize URL handed back to the user's browser is resolvable from outside the server's network:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.106.2",
3
+ "version": "0.107.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",
@@ -106,6 +106,27 @@
106
106
  "toolName": "create-shared-mailbox-draft",
107
107
  "workScopes": ["Mail.ReadWrite.Shared"]
108
108
  },
109
+ {
110
+ "pathPattern": "/users/{user-id}/messages/{message-id}/reply",
111
+ "method": "post",
112
+ "toolName": "reply-shared-mailbox-mail",
113
+ "workScopes": ["Mail.Send.Shared"],
114
+ "llmTip": "Reply to a message from a shared mailbox preserving full HTML formatting and conversation thread. The 'user-id' is the shared mailbox email address (e.g. support@contoso.com). The 'comment' field is your reply text. Do NOT reconstruct the email manually. Requires Send As permission on the shared mailbox in Exchange."
115
+ },
116
+ {
117
+ "pathPattern": "/users/{user-id}/messages/{message-id}/replyAll",
118
+ "method": "post",
119
+ "toolName": "reply-all-shared-mailbox-mail",
120
+ "workScopes": ["Mail.Send.Shared"],
121
+ "llmTip": "Reply-all to a message from a shared mailbox preserving full HTML formatting and conversation thread. The 'user-id' is the shared mailbox email address. The 'comment' field is your reply text. Requires Send As permission on the shared mailbox in Exchange."
122
+ },
123
+ {
124
+ "pathPattern": "/users/{user-id}/messages/{message-id}/forward",
125
+ "method": "post",
126
+ "toolName": "forward-shared-mailbox-mail",
127
+ "workScopes": ["Mail.Send.Shared"],
128
+ "llmTip": "Forward a message from a shared mailbox preserving full HTML formatting and attachments. The 'user-id' is the shared mailbox email address. 'toRecipients' is required. The 'comment' field adds text above the forwarded content. Requires Send As permission on the shared mailbox in Exchange."
129
+ },
109
130
  {
110
131
  "pathPattern": "/users",
111
132
  "method": "get",