@softeria/ms-365-mcp-server 0.161.3 → 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
@@ -633,7 +633,7 @@ npx @softeria/ms-365-mcp-server --list-presets # See all available presets
633
633
 
634
634
  Available presets: `mail`, `calendar`, `files`, `personal`, `work`, `excel`, `contacts`, `tasks`, `onenote`, `search`, `users`, `outlook`, `onedrive`, `teams`, `teams-write`, `all`
635
635
 
636
- Each endpoint in `endpoints.json` declares which presets it belongs to via a `presets` array, so every preset is an exact tool-name allow-list that never over-matches across apps (e.g. `mail` does not include shared-mailbox tools; those are in `work`). The universal binary reader `download-bytes` is included in every preset except `teams-write`, so whatever an app returns (a file, an attachment, a photo, a recording) can always be fetched; `get-download-url` (a pre-authenticated URL for drive/SharePoint files) rides with the drive-backed presets. So a preset that can find a file can always read its bytes.
636
+ Each endpoint in `endpoints.json` declares which presets it belongs to via a `presets` array, so every preset is an exact tool-name allow-list that never over-matches across apps (e.g. `mail` does not include shared-mailbox tools; those are in `work`). The universal binary reader `download-bytes` is included in every preset except `teams-write`, so whatever an app returns (a file, an attachment, a photo, a recording) can always be fetched; `get-download-url` (a pre-authenticated URL for drive/SharePoint files) rides with the drive-backed presets, and joins every other preset (bar `teams-write`) when HTTP mode runs with `--enable-attachment-urls`. So a preset that can find a file can always read its bytes.
637
637
 
638
638
  The `outlook`, `onedrive` and `teams` presets are app-scoped: they expose exactly one Microsoft app. Use these for "expose exactly one app" deployments:
639
639
 
@@ -699,7 +699,8 @@ When running as an MCP server, the following options can be used:
699
699
  --trust-proxy-auth
700
700
  --enable-auth-tools Enable login/logout tools when using HTTP mode (disabled by default in HTTP mode)
701
701
  --enable-attachment-urls Let get-download-url mint a server-served URL for byte resources Graph
702
- exposes no pre-authenticated URL for (see "Server-Minted Attachment URLs")
702
+ exposes no pre-authenticated URL for (see "Server-Minted Attachment URLs").
703
+ Also adds get-download-url to any --preset that lacks it (except teams-write)
703
704
  --attachment-port <port> Serve /attachment on its own listener on this port instead of on the
704
705
  MCP app, so a fetcher that can read attachments cannot also reach /mcp
705
706
  (requires --enable-attachment-urls; see "Splitting the attachment listener")
@@ -761,7 +762,13 @@ agent's context. A 73 KB, 3-page PDF costs about 24,500 tokens that way, and the
761
762
  cannot parse them anyway.
762
763
 
763
764
  `--enable-attachment-urls` (HTTP mode, off by default) closes that gap. When Graph has no
764
- URL of its own, `get-download-url` mints one this server serves:
765
+ URL of its own, `get-download-url` mints one this server serves. With it on,
766
+ `get-download-url` is registered under every `--preset` (except the send-only `teams-write`),
767
+ so a `--preset mail` agent that lists an attachment can mint a URL for it. A minted URL only
768
+ works if the preset's scopes cover the resource: a contacts, tasks or excel preset has no Mail
769
+ scopes, so a URL minted for a mail attachment fails when fetched.
770
+
771
+ The minted URL looks like:
765
772
 
766
773
  ```
767
774
  GET /attachment?t=<ticket>&dgk=<key-id>&dgx=<expiry>&dgs=<signature>
@@ -802,16 +809,21 @@ is refused with the same 404 as any other bad ticket.
802
809
 
803
810
  ```
804
811
  MS365_MCP_ATTACHMENT_URL_BASE=http://m365-mcp:3000 # required
805
- MS365_MCP_ATTACHMENT_URL_KEY=... # required (or _KEY_FILE=/path)
806
- 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
807
814
  MS365_MCP_ATTACHMENT_URL_TTL_S=120 # optional, default 120, max 300
808
815
  ```
809
816
 
810
817
  `MS365_MCP_ATTACHMENT_URL_BASE` is deliberately **not** `MS365_MCP_PUBLIC_URL`: that one is
811
818
  browser-facing, for OAuth redirects, while this is fetched server-to-server and is
812
819
  commonly a container address. A missing or malformed setting fails at startup rather than
813
- per-request — a signing feature that comes up without a key would mint URLs nothing can
814
- 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.
815
827
 
816
828
  ### Splitting the attachment listener
817
829
 
@@ -897,9 +909,10 @@ directly.
897
909
  `--attachment-host` takes a bare IPv4 address, IPv6 address (bracketed `[::1]` or bare
898
910
  `::1`) or hostname. It is refused rather than coerced — `--attachment-host 10.0.0.5:3001`
899
911
  is an error naming `--attachment-port`, not a bind to something else. Note that
900
- `MS365_MCP_ATTACHMENT_URL_BASE` still must not be an IPv6 literal (the URL signature covers
901
- the host and the two implementations normalise IPv6 differently); if you bind the listener
902
- 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.
903
916
 
904
917
  Point `MS365_MCP_ATTACHMENT_URL_BASE` at the attachment port. The server cannot check this
905
918
  for you: the base is usually a container name on a network this process cannot resolve, so
@@ -908,7 +921,7 @@ base and the bound port are logged at startup, one line apart, for exactly that
908
921
 
909
922
  ### The signature, and who checks what
910
923
 
911
- `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.
912
925
  They exist for the fetcher: a document-conversion sidecar that refuses to dial a private
913
926
  address unless the URL carries a valid HMAC from an origin it has been configured to trust.
914
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). 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."
@@ -106,7 +106,9 @@ function parseArgs() {
106
106
  if (options.preset) {
107
107
  const presetNames = options.preset.split(",").map((p) => p.trim());
108
108
  try {
109
- options.enabledTools = getCombinedPresetPattern(presetNames);
109
+ options.enabledTools = getCombinedPresetPattern(presetNames, {
110
+ attachmentUrls: Boolean(options.http && options.enableAttachmentUrls)
111
+ });
110
112
  const requiresOrgMode = presetNames.some((preset) => presetRequiresOrgMode(preset));
111
113
  if (requiresOrgMode && !options.orgMode) {
112
114
  console.warn(
@@ -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(
@@ -101,7 +101,7 @@ const TOOL_CATEGORIES = {
101
101
  description: "All available tools"
102
102
  }
103
103
  };
104
- function getCombinedPresetPattern(presets) {
104
+ function getCombinedPresetPattern(presets, { attachmentUrls = false } = {}) {
105
105
  const patterns = presets.map((preset) => {
106
106
  const category = TOOL_CATEGORIES[preset];
107
107
  if (!category) {
@@ -111,6 +111,9 @@ function getCombinedPresetPattern(presets) {
111
111
  }
112
112
  return category.pattern.source;
113
113
  });
114
+ if (attachmentUrls && presets.some((p) => !PRESET_META[p]?.omitUniversalUtilities) && !patterns.some((p) => new RegExp(p).test("get-download-url"))) {
115
+ patterns.push("^get-download-url$");
116
+ }
114
117
  return patterns.join("|");
115
118
  }
116
119
  function listPresets() {
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.3",
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",