@softeria/ms-365-mcp-server 0.161.4 → 0.162.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.
package/README.md CHANGED
@@ -809,16 +809,21 @@ is refused with the same 404 as any other bad ticket.
809
809
 
810
810
  ```
811
811
  MS365_MCP_ATTACHMENT_URL_BASE=http://m365-mcp:3000 # required
812
- MS365_MCP_ATTACHMENT_URL_KEY=... # required (or _KEY_FILE=/path)
813
- MS365_MCP_ATTACHMENT_URL_KEY_ID=1 # optional, default 1
812
+ MS365_MCP_ATTACHMENT_URL_KEY=... # optional (or _KEY_FILE=/path), see below
813
+ MS365_MCP_ATTACHMENT_URL_KEY_ID=1 # optional, default 1; needs a key
814
814
  MS365_MCP_ATTACHMENT_URL_TTL_S=120 # optional, default 120, max 300
815
815
  ```
816
816
 
817
817
  `MS365_MCP_ATTACHMENT_URL_BASE` is deliberately **not** `MS365_MCP_PUBLIC_URL`: that one is
818
818
  browser-facing, for OAuth redirects, while this is fetched server-to-server and is
819
819
  commonly a container address. A missing or malformed setting fails at startup rather than
820
- per-request — a signing feature that comes up without a key would mint URLs nothing can
821
- verify, silently.
820
+ per-request.
821
+
822
+ The signing key is optional. Without one, minted URLs are just `/attachment?t=<ticket>` and
823
+ any HTTP client can fetch them; the ticket alone authorises redemption. Set a key only if
824
+ the fetcher verifies the `dgk`/`dgx`/`dgs` signature (see below), such as a docglean-mcp
825
+ sidecar, which will refuse an unsigned URL. Setting `_KEY_ID` without a key, or a `_KEY_FILE` that is
826
+ empty or unreadable, fails at startup.
822
827
 
823
828
  ### Splitting the attachment listener
824
829
 
@@ -904,9 +909,10 @@ directly.
904
909
  `--attachment-host` takes a bare IPv4 address, IPv6 address (bracketed `[::1]` or bare
905
910
  `::1`) or hostname. It is refused rather than coerced — `--attachment-host 10.0.0.5:3001`
906
911
  is an error naming `--attachment-port`, not a bind to something else. Note that
907
- `MS365_MCP_ATTACHMENT_URL_BASE` still must not be an IPv6 literal (the URL signature covers
908
- the host and the two implementations normalise IPv6 differently); if you bind the listener
909
- to an IPv6 address, name it in the base by hostname.
912
+ with a signing key set, `MS365_MCP_ATTACHMENT_URL_BASE` must not be an IPv6 literal (the URL
913
+ signature covers the host and the two implementations normalise IPv6 differently); if you
914
+ bind the listener to an IPv6 address, name it in the base by hostname. Without a key there
915
+ is no such restriction.
910
916
 
911
917
  Point `MS365_MCP_ATTACHMENT_URL_BASE` at the attachment port. The server cannot check this
912
918
  for you: the base is usually a container name on a network this process cannot resolve, so
@@ -915,7 +921,7 @@ base and the bound port are logged at startup, one line apart, for exactly that
915
921
 
916
922
  ### The signature, and who checks what
917
923
 
918
- `dgk`/`dgx`/`dgs` are **not** checked by this server on redemption, and that is deliberate.
924
+ This server never checks `dgk`/`dgx`/`dgs` on redemption, and that is deliberate.
919
925
  They exist for the fetcher: a document-conversion sidecar that refuses to dial a private
920
926
  address unless the URL carries a valid HMAC from an origin it has been configured to trust.
921
927
  What authorises redemption _here_ is the ticket. Verifying the signature on the way back in
package/dist/cli.js CHANGED
@@ -29,7 +29,7 @@ program.name("ms-365-mcp-server").description("Microsoft 365 MCP Server").versio
29
29
  "Enable login/logout tools when using HTTP mode (disabled by default in HTTP mode)"
30
30
  ).option(
31
31
  "--enable-attachment-urls",
32
- "HTTP mode only. Let get-download-url mint a short-TTL, single-use URL served by this server for Graph byte resources that expose no pre-authenticated URL of their own (mail and event attachments, meeting recordings, other $value endpoints). Also adds get-download-url to any --preset that lacks it (except teams-write). Requires MS365_MCP_ATTACHMENT_URL_BASE and MS365_MCP_ATTACHMENT_URL_KEY (or _KEY_FILE)"
32
+ "HTTP mode only. Let get-download-url mint a short-TTL, single-use URL served by this server for Graph byte resources that expose no pre-authenticated URL of their own (mail and event attachments, meeting recordings, other $value endpoints). Also adds get-download-url to any --preset that lacks it (except teams-write). Requires MS365_MCP_ATTACHMENT_URL_BASE; MS365_MCP_ATTACHMENT_URL_KEY (or _KEY_FILE) is optional and adds an HMAC signature for fetchers that verify one"
33
33
  ).option(
34
34
  "--attachment-port <port>",
35
35
  "HTTP mode only. Serve the attachment download route on its own listener on this port instead of on the MCP app, so a caller that can fetch attachments cannot also reach /mcp. Requires --enable-attachment-urls, and MS365_MCP_ATTACHMENT_URL_BASE must name this port. A separate port only isolates the two surfaces if they also bind separate interfaces \u2014 see --attachment-host. Equivalent env var: MS365_MCP_ATTACHMENT_PORT."
@@ -5,6 +5,7 @@ const TICKET_PARAM = "t";
5
5
  function buildAttachmentUrl(config, ticketId, nowMs = Date.now()) {
6
6
  const url = new URL(ATTACHMENT_ROUTE, config.base);
7
7
  url.searchParams.set(TICKET_PARAM, ticketId);
8
+ if (!config.key) return url.toString();
8
9
  return signUrl(
9
10
  url.toString(),
10
11
  { key: config.key, keyId: config.keyId, ttlSeconds: config.ttlSeconds },
@@ -19,13 +19,20 @@ function pythonStrip(value) {
19
19
  function readKey(env) {
20
20
  const keyFile = env.MS365_MCP_ATTACHMENT_URL_KEY_FILE;
21
21
  if (keyFile) {
22
+ let key;
22
23
  try {
23
- return pythonStrip(readFileSync(keyFile, "utf8"));
24
+ key = pythonStrip(readFileSync(keyFile, "utf8"));
24
25
  } catch {
25
26
  throw new AttachmentUrlConfigError(
26
27
  `MS365_MCP_ATTACHMENT_URL_KEY_FILE could not be read: ${keyFile}`
27
28
  );
28
29
  }
30
+ if (!key) {
31
+ throw new AttachmentUrlConfigError(
32
+ `MS365_MCP_ATTACHMENT_URL_KEY_FILE is empty: ${keyFile}. Unset it to mint unsigned URLs.`
33
+ );
34
+ }
35
+ return key;
29
36
  }
30
37
  return env.MS365_MCP_ATTACHMENT_URL_KEY ?? "";
31
38
  }
@@ -50,20 +57,26 @@ function loadAttachmentUrlConfig(enabled, env = process.env) {
50
57
  `MS365_MCP_ATTACHMENT_URL_BASE must be http or https, got ${parsedBase.protocol}`
51
58
  );
52
59
  }
53
- if (parsedBase.hostname.startsWith("[")) {
54
- throw new AttachmentUrlConfigError(
55
- `MS365_MCP_ATTACHMENT_URL_BASE must not be an IPv6 literal: the URL signature covers the host, and this runtime and the verifying sidecar normalise IPv6 spellings differently, so every minted URL would be refused. Use a hostname (a container or service name) instead of ${parsedBase.hostname}.`
56
- );
57
- }
58
60
  if (parsedBase.search || parsedBase.hash) {
59
61
  throw new AttachmentUrlConfigError(
60
62
  "MS365_MCP_ATTACHMENT_URL_BASE must not carry a query string or fragment."
61
63
  );
62
64
  }
63
65
  const key = readKey(env);
64
- if (!key) {
66
+ if (key && !key.trim()) {
67
+ throw new AttachmentUrlConfigError(
68
+ "MS365_MCP_ATTACHMENT_URL_KEY is whitespace only. Set a real key, or unset it to mint unsigned URLs."
69
+ );
70
+ }
71
+ if (key) {
72
+ if (parsedBase.hostname.startsWith("[")) {
73
+ throw new AttachmentUrlConfigError(
74
+ `MS365_MCP_ATTACHMENT_URL_BASE must not be an IPv6 literal: the URL signature covers the host, and this runtime and the verifying sidecar normalise IPv6 spellings differently, so every minted URL would be refused. Use a hostname (a container or service name) instead of ${parsedBase.hostname}.`
75
+ );
76
+ }
77
+ } else if (env.MS365_MCP_ATTACHMENT_URL_KEY_ID) {
65
78
  throw new AttachmentUrlConfigError(
66
- "--enable-attachment-urls requires MS365_MCP_ATTACHMENT_URL_KEY or MS365_MCP_ATTACHMENT_URL_KEY_FILE -- the HMAC key shared with the sidecar that will verify the minted URL."
79
+ "MS365_MCP_ATTACHMENT_URL_KEY_ID is set but no signing key is: set MS365_MCP_ATTACHMENT_URL_KEY or MS365_MCP_ATTACHMENT_URL_KEY_FILE, or unset the key id."
67
80
  );
68
81
  }
69
82
  for (let index = 0; index < key.length; index += 1) {
@@ -92,7 +105,7 @@ function loadAttachmentUrlConfig(enabled, env = process.env) {
92
105
  }
93
106
  return {
94
107
  base: parsedBase.origin,
95
- key,
108
+ key: key || void 0,
96
109
  keyId,
97
110
  ttlSeconds
98
111
  };
package/dist/server.js CHANGED
@@ -730,7 +730,7 @@ class MicrosoftGraphServer {
730
730
  attachmentApp.get(ATTACHMENT_ROUTE, createAttachmentHandler(attachmentDeps));
731
731
  attachmentApp.put(ATTACHMENT_ROUTE, createAttachmentUploadHandler(attachmentDeps));
732
732
  logger.info(
733
- ` - Attachment URLs: ${attachmentConfig.base}${ATTACHMENT_ROUTE} (ttl ${attachmentConfig.ttlSeconds}s, key id ${attachmentConfig.keyId})`
733
+ ` - Attachment URLs: ${attachmentConfig.base}${ATTACHMENT_ROUTE} (ttl ${attachmentConfig.ttlSeconds}s, ${attachmentConfig.key ? `signed, key id ${attachmentConfig.keyId}` : "unsigned"})`
734
734
  );
735
735
  if (dedicated) {
736
736
  logger.info(
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.161.4",
4
+ "version": "0.162.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",