@softeria/ms-365-mcp-server 0.155.0 → 0.156.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 +10 -1
- package/dist/cli.js +6 -0
- package/dist/graph-tools.js +1 -1
- package/dist/mcp-instructions.js +1 -1
- package/dist/server.js +66 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -610,7 +610,15 @@ When running as an MCP server, the following options can be used:
|
|
|
610
610
|
-v Enable verbose logging
|
|
611
611
|
--read-only Start server in read-only mode, disabling write operations
|
|
612
612
|
--http [port] Use Streamable HTTP transport instead of stdio (optionally specify port, default: 3000)
|
|
613
|
-
Starts Express.js server with MCP endpoint at /mcp
|
|
613
|
+
Starts Express.js server with MCP endpoint at /mcp. Bound to a loopback host
|
|
614
|
+
(e.g. --http 127.0.0.1:3000 or --http [::1]:3000) with no --public-url, it
|
|
615
|
+
rejects requests whose Host or Origin is not localhost (not applied to the
|
|
616
|
+
--attachment-port listener)
|
|
617
|
+
--http-local-file-tools Register download-bytes-to-file over HTTP. Anyone who can reach the port
|
|
618
|
+
can write files as the server's user (without a valid token, only an empty
|
|
619
|
+
file that is removed again), so enable it only on a single-user machine.
|
|
620
|
+
Refused unless --http binds a loopback host with no --public-url and no
|
|
621
|
+
--trust-proxy-auth
|
|
614
622
|
--enable-auth-tools Enable login/logout tools when using HTTP mode (disabled by default in HTTP mode)
|
|
615
623
|
--enable-attachment-urls Let get-download-url mint a server-served URL for byte resources Graph
|
|
616
624
|
exposes no pre-authenticated URL for (see "Server-Minted Attachment URLs")
|
|
@@ -648,6 +656,7 @@ Environment variables:
|
|
|
648
656
|
- `MS365_MCP_TRUST_PROXY_HOPS=<n>`: Number of trusted reverse-proxy hops in HTTP mode (default `1`). Accurate per-IP rate limiting depends on this matching your deployment — set to the number of proxies in front of the server, `0` to use the raw socket peer IP, or a comma-separated subnet list
|
|
649
657
|
- `MS365_MCP_ATTACHMENT_PORT=<port>`: Serve the attachment route on its own listener on this port (alternative to --attachment-port; requires `--enable-attachment-urls`)
|
|
650
658
|
- `MS365_MCP_ATTACHMENT_HOST=<host>`: Interface the `MS365_MCP_ATTACHMENT_PORT` listener binds (alternative to --attachment-host; requires `--attachment-port`). Defaults to the host `--http` bound — which for a wildcard `--http` means both ports answer everywhere and the port split isolates nothing. See "Splitting the attachment listener"
|
|
659
|
+
- `MS365_MCP_HTTP_LOCAL_FILE_TOOLS=true|1`: Register download-bytes-to-file over HTTP (alternative to --http-local-file-tools; same restrictions)
|
|
651
660
|
- `MS365_MCP_CLOUD_TYPE=global|china`: Microsoft cloud environment (alternative to --cloud flag)
|
|
652
661
|
- `LOG_LEVEL`: Set logging level (default: 'info')
|
|
653
662
|
- `SILENT=true|1`: Disable console output
|
package/dist/cli.js
CHANGED
|
@@ -69,6 +69,9 @@ program.name("ms-365-mcp-server").description("Microsoft 365 MCP Server").versio
|
|
|
69
69
|
).option(
|
|
70
70
|
"--trust-proxy-auth",
|
|
71
71
|
"In HTTP mode, skip the built-in Bearer-token check on /mcp and ignore any forwarded Authorization header. All callers share the locally cached MSAL identity (same path stdio mode uses). Use only when an upstream reverse proxy has already authenticated the caller."
|
|
72
|
+
).option(
|
|
73
|
+
"--http-local-file-tools",
|
|
74
|
+
"In HTTP mode, also register download-bytes-to-file, which writes files on the server as the server's user. Anyone who can reach the port can use it, so enable only on a single-user machine. Refused unless --http binds a loopback host with no --public-url and no --trust-proxy-auth."
|
|
72
75
|
).option(
|
|
73
76
|
"--allow-unauthenticated-discovery",
|
|
74
77
|
"In HTTP mode, allow MCP discovery requests (initialize, tools/list, prompts/list, resources/list, ping) without a bearer token, so a gateway can enumerate the tool catalog before any user has authenticated. Non-discovery requests (e.g. tools/call) still require a token. Off by default."
|
|
@@ -204,6 +207,9 @@ function parseArgs() {
|
|
|
204
207
|
if (process.env.MS365_MCP_TRUST_PROXY_AUTH === "true" || process.env.MS365_MCP_TRUST_PROXY_AUTH === "1") {
|
|
205
208
|
options.trustProxyAuth = true;
|
|
206
209
|
}
|
|
210
|
+
if (process.env.MS365_MCP_HTTP_LOCAL_FILE_TOOLS === "true" || process.env.MS365_MCP_HTTP_LOCAL_FILE_TOOLS === "1") {
|
|
211
|
+
options.httpLocalFileTools = true;
|
|
212
|
+
}
|
|
207
213
|
if (process.env.MS365_MCP_ALLOW_UNAUTHENTICATED_DISCOVERY === "true" || process.env.MS365_MCP_ALLOW_UNAUTHENTICATED_DISCOVERY === "1") {
|
|
208
214
|
options.allowUnauthenticatedDiscovery = true;
|
|
209
215
|
}
|
package/dist/graph-tools.js
CHANGED
|
@@ -699,7 +699,7 @@ const UTILITY_TOOLS = [
|
|
|
699
699
|
// description at ~40 tokens, so the OneDrive/SharePoint guidance below
|
|
700
700
|
// sits past the cap. That keeps the hint for the reading LLM while letting
|
|
701
701
|
// get-download-url own the high-signal "drive"/"sharepoint" search terms.
|
|
702
|
-
description: "Write authenticated Microsoft Graph byte content to a local file on the server, returning { path, contentType, bytesWritten } instead of base64. The only out-of-band way to save mail attachments and meeting recordings, whose bytes are exposed solely through authenticated endpoints. Also handles profile photos and Teams hosted content. Writes to an absolute outputPath and never overwrites an existing file. stdio mode
|
|
702
|
+
description: "Write authenticated Microsoft Graph byte content to a local file on the server, returning { path, contentType, bytesWritten } instead of base64. The only out-of-band way to save mail attachments and meeting recordings, whose bytes are exposed solely through authenticated endpoints. Also handles profile photos and Teams hosted content. Writes to an absolute outputPath and never overwrites an existing file. stdio mode, or HTTP with --http-local-file-tools. For OneDrive or SharePoint file content, get-download-url is preferred \u2014 it returns a pre-authenticated URL for fully out-of-band download without the server fetching the bytes.",
|
|
703
703
|
readOnlyHint: true,
|
|
704
704
|
openWorldHint: true,
|
|
705
705
|
stdioOnly: true,
|
package/dist/mcp-instructions.js
CHANGED
|
@@ -6,7 +6,7 @@ function buildGeneralMcpInstructions(opts) {
|
|
|
6
6
|
"When you need an organizational user or recipient address, resolve it with list-users (or another directory tool); do not invent SMTP addresses.",
|
|
7
7
|
"Directory $search on collections such as /users or /groups requires ConsistencyLevel: eventual when the tool exposes that header.",
|
|
8
8
|
"Teams chat and channel messages: prefer HTML contentType in the body; plain text is often mangled by Graph.",
|
|
9
|
-
"Files / binary content: for large drive/SharePoint file content, prefer get-download-url to resolve a pre-authenticated URL for out-of-band download. Use download-bytes for authenticated byte reads such as mail attachments, profile photos, Teams hosted content, and meeting recordings. In stdio mode, download-bytes-to-file writes those same authenticated bytes straight to a local absolute path instead of returning base64 \u2014 the only out-of-band option for large mail attachments and meeting recordings, which get-download-url cannot handle. These tools take relative Microsoft Graph paths, not absolute URLs. For uploads, upload-file-content takes a base64 string body (Graph allows 250MB, but the whole string passes through the agent context and a truncated one is written without error); use create-upload-session for anything but small files."
|
|
9
|
+
"Files / binary content: for large drive/SharePoint file content, prefer get-download-url to resolve a pre-authenticated URL for out-of-band download. Use download-bytes for authenticated byte reads such as mail attachments, profile photos, Teams hosted content, and meeting recordings. In stdio mode (or HTTP with --http-local-file-tools), download-bytes-to-file writes those same authenticated bytes straight to a local absolute path instead of returning base64 \u2014 the only out-of-band option for large mail attachments and meeting recordings, which get-download-url cannot handle. These tools take relative Microsoft Graph paths, not absolute URLs. For uploads, upload-file-content takes a base64 string body (Graph allows 250MB, but the whole string passes through the agent context and a truncated one is written without error); use create-upload-session for anything but small files."
|
|
10
10
|
];
|
|
11
11
|
if (opts.readOnly) parts.push("This server is read-only; write operations are disabled.");
|
|
12
12
|
if (opts.multiAccount)
|
package/dist/server.js
CHANGED
|
@@ -36,11 +36,40 @@ import { dumpError } from "./crash-logging.js";
|
|
|
36
36
|
import crypto from "node:crypto";
|
|
37
37
|
import { isIP, isIPv6 } from "node:net";
|
|
38
38
|
import OboClient from "./obo-client.js";
|
|
39
|
+
import { hostHeaderValidation } from "@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js";
|
|
40
|
+
const LOOPBACK_HOSTNAMES = ["localhost", "127.0.0.1", "[::1]"];
|
|
41
|
+
function isLoopbackHost(host) {
|
|
42
|
+
if (!host) return false;
|
|
43
|
+
const bare = host.toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
44
|
+
return bare === "localhost" || bare === "127.0.0.1" || bare === "::1";
|
|
45
|
+
}
|
|
46
|
+
function loopbackOriginValidation() {
|
|
47
|
+
return (req, res, next) => {
|
|
48
|
+
const origin = req.headers.origin;
|
|
49
|
+
if (origin === void 0) return next();
|
|
50
|
+
let hostname;
|
|
51
|
+
try {
|
|
52
|
+
hostname = new URL(origin).hostname;
|
|
53
|
+
} catch {
|
|
54
|
+
hostname = void 0;
|
|
55
|
+
}
|
|
56
|
+
if (hostname && LOOPBACK_HOSTNAMES.includes(hostname)) return next();
|
|
57
|
+
res.status(403).json({
|
|
58
|
+
jsonrpc: "2.0",
|
|
59
|
+
error: { code: -32e3, message: `Invalid Origin: ${origin}` },
|
|
60
|
+
id: null
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
}
|
|
39
64
|
function parseHttpOption(httpOption) {
|
|
40
65
|
if (typeof httpOption === "boolean") {
|
|
41
66
|
return { host: void 0, port: 3e3 };
|
|
42
67
|
}
|
|
43
68
|
const httpString = httpOption.trim();
|
|
69
|
+
const bracketed = /^\[([^\]]+)\](?::(.*))?$/.exec(httpString);
|
|
70
|
+
if (bracketed) {
|
|
71
|
+
return { host: bracketed[1], port: parseInt(bracketed[2] ?? "") || 3e3 };
|
|
72
|
+
}
|
|
44
73
|
if (httpString.includes(":")) {
|
|
45
74
|
const [hostPart, portPart] = httpString.split(":");
|
|
46
75
|
const host = hostPart || void 0;
|
|
@@ -131,6 +160,14 @@ class MicrosoftGraphServer {
|
|
|
131
160
|
this.secrets = null;
|
|
132
161
|
this.oboClient = null;
|
|
133
162
|
}
|
|
163
|
+
isLoopbackOnlyHttp() {
|
|
164
|
+
if (!this.options.http) return false;
|
|
165
|
+
const publicUrl = this.options.publicUrl || process.env.MS365_MCP_PUBLIC_URL || this.options.baseUrl || process.env.MS365_MCP_BASE_URL;
|
|
166
|
+
return !publicUrl && isLoopbackHost(parseHttpOption(this.options.http).host);
|
|
167
|
+
}
|
|
168
|
+
hidesStdioOnlyTools() {
|
|
169
|
+
return Boolean(this.options.http) && !this.options.httpLocalFileTools;
|
|
170
|
+
}
|
|
134
171
|
createMcpServer() {
|
|
135
172
|
const server = new McpServer(
|
|
136
173
|
{
|
|
@@ -161,7 +198,7 @@ class MicrosoftGraphServer {
|
|
|
161
198
|
this.accountNames,
|
|
162
199
|
this.options.enabledTools,
|
|
163
200
|
this.options.allowedScopes,
|
|
164
|
-
|
|
201
|
+
this.hidesStdioOnlyTools()
|
|
165
202
|
);
|
|
166
203
|
} else {
|
|
167
204
|
registerGraphTools(
|
|
@@ -174,7 +211,7 @@ class MicrosoftGraphServer {
|
|
|
174
211
|
this.multiAccount,
|
|
175
212
|
this.accountNames,
|
|
176
213
|
this.options.allowedScopes,
|
|
177
|
-
|
|
214
|
+
this.hidesStdioOnlyTools()
|
|
178
215
|
);
|
|
179
216
|
}
|
|
180
217
|
installToolSchemaRefNormalization(server);
|
|
@@ -202,6 +239,25 @@ class MicrosoftGraphServer {
|
|
|
202
239
|
'Account routing disabled: requests use the OAuth bearer identity, so the "account" parameter is not injected into tool schemas'
|
|
203
240
|
);
|
|
204
241
|
}
|
|
242
|
+
if (this.options.httpLocalFileTools && this.options.http) {
|
|
243
|
+
if (!this.isLoopbackOnlyHttp()) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
"--http-local-file-tools requires --http bound to a loopback host (localhost, 127.0.0.1 or [::1]) and no --public-url: anyone who can reach the port can write files as the server's user."
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (this.options.trustProxyAuth) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
"--http-local-file-tools cannot be combined with --trust-proxy-auth: a proxy in front of the loopback port would let remote callers write files as the server's user."
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
logger.warn(
|
|
254
|
+
"--http-local-file-tools: download-bytes-to-file is registered over HTTP; anyone who can reach this port can write files as the server's user."
|
|
255
|
+
);
|
|
256
|
+
} else if (this.options.httpLocalFileTools) {
|
|
257
|
+
logger.warn(
|
|
258
|
+
"--http-local-file-tools has no effect in stdio mode, where download-bytes-to-file is always registered."
|
|
259
|
+
);
|
|
260
|
+
}
|
|
205
261
|
if (this.options.obo) {
|
|
206
262
|
if (!this.options.http) {
|
|
207
263
|
throw new Error("--obo requires --http (On-Behalf-Of flow only works in HTTP mode).");
|
|
@@ -267,6 +323,10 @@ class MicrosoftGraphServer {
|
|
|
267
323
|
if (this.options.http) {
|
|
268
324
|
const { host, port } = parseHttpOption(this.options.http);
|
|
269
325
|
const app = express();
|
|
326
|
+
if (this.isLoopbackOnlyHttp()) {
|
|
327
|
+
app.use(hostHeaderValidation(LOOPBACK_HOSTNAMES));
|
|
328
|
+
app.use(loopbackOriginValidation());
|
|
329
|
+
}
|
|
270
330
|
const trustProxyEnv = process.env.MS365_MCP_TRUST_PROXY_HOPS;
|
|
271
331
|
if (trustProxyEnv !== void 0 && trustProxyEnv !== "") {
|
|
272
332
|
const asNum = Number(trustProxyEnv);
|
|
@@ -803,6 +863,9 @@ class MicrosoftGraphServer {
|
|
|
803
863
|
var server_default = MicrosoftGraphServer;
|
|
804
864
|
export {
|
|
805
865
|
server_default as default,
|
|
866
|
+
isLoopbackHost,
|
|
867
|
+
loopbackOriginValidation,
|
|
806
868
|
parseAttachmentHostOption,
|
|
807
|
-
parseAttachmentPortOption
|
|
869
|
+
parseAttachmentPortOption,
|
|
870
|
+
parseHttpOption
|
|
808
871
|
};
|
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.156.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",
|