@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 +14 -8
- package/dist/cli.js +1 -1
- package/dist/lib/attachment-tickets.js +1 -0
- package/dist/lib/attachment-url-config.js +22 -9
- package/dist/server.js +1 -1
- package/package.json +1 -1
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=... #
|
|
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
|
|
821
|
-
|
|
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`
|
|
908
|
-
the host and the two implementations normalise IPv6 differently); if you
|
|
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`
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
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.
|
|
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",
|