@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 +24 -11
- package/dist/cli.js +4 -2
- package/dist/lib/attachment-tickets.js +1 -0
- package/dist/lib/attachment-url-config.js +22 -9
- package/dist/server.js +1 -1
- package/dist/tool-categories.js +4 -1
- package/package.json +1 -1
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
|
|
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=... #
|
|
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
|
|
814
|
-
|
|
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`
|
|
901
|
-
the host and the two implementations normalise IPv6 differently); if you
|
|
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`
|
|
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
|
|
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
|
-
|
|
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/dist/tool-categories.js
CHANGED
|
@@ -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.
|
|
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",
|