@softeria/ms-365-mcp-server 0.165.0 → 0.166.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 +1 -0
- package/dist/lib/microsoft-auth.js +1 -0
- package/dist/lib/obo-access.js +50 -0
- package/dist/server.js +25 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -735,6 +735,7 @@ Environment variables:
|
|
|
735
735
|
- `MS365_MCP_BLOB_OUTPUT=true|1`: File download tools (`download-bytes`, `get-mail-message-mime`, `get-shared-mailbox-message-mime`, `get-mail-attachment-content`, `get-shared-mailbox-attachment-content`) answer a short JSON text block (`name`, `contentType`, `size`) and an MCP `EmbeddedResource` with `BlobResourceContents` (`uri`, `mimeType`, `blob`) instead of base64 JSON in text. The `uri`'s last segment is the optional `fileName` argument. Default: off. Alternative to the --blob-output flag.
|
|
736
736
|
- `MS365_MCP_BLOB_MAX_BYTES=<n>`: With blob output on, the largest file a download tool reads (positive integer, default `5242880`). The server stops reading past it and answers an error naming the limit.
|
|
737
737
|
- `MS365_MCP_BODY_FORMAT=html`: Return email bodies as HTML instead of plain text (default: text)
|
|
738
|
+
- `MS365_MCP_OBO_ALLOWED_TENANTS=<ids>` / `MS365_MCP_OBO_ALLOWED_USERS=<ids>`: With `--obo`, only callers whose token `tid` (tenant id) / `oid` (user object id) is listed may use the server; others get 403 before any On-Behalf-Of exchange. Comma or space separated; unset = everyone. The claims are read without verifying the signature, which Entra verifies during the exchange, so a token that lies about them still gets no Graph token. A token Entra rejects at the exchange (bad signature, expired, wrong audience, consent needed) gets 401 with a `WWW-Authenticate` challenge so the client signs in again
|
|
738
739
|
- `MS365_MCP_MESSAGE_SIGNOFF_PREFIX=<text>`: Signoff prepended to outgoing messages so recipients can tell they were agent-sent, e.g. `🤖`. Default: none. CLI equivalent: `--message-signoff-prefix <text>` (see Message Signoff below)
|
|
739
740
|
- `MS365_MCP_MESSAGE_SIGNOFF_SUFFIX=<text>`: Signoff appended to outgoing messages. Default: none. CLI equivalent: `--message-signoff-suffix <text>`. `--no-message-signoff` disables both (see Message Signoff below)
|
|
740
741
|
- `MS365_MCP_RATE_LIMIT_DISABLED=true|1`: Disable per-IP rate limiting in HTTP mode (default: enabled — 30 req/min on `/authorize`, `/token`, `/register`; 120 req/min on `/mcp`)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const splitList = (raw) => (raw ?? "").split(/[\s,]+/).map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
2
|
+
function readOboAllowlist(env = process.env) {
|
|
3
|
+
return {
|
|
4
|
+
tenants: new Set(splitList(env.MS365_MCP_OBO_ALLOWED_TENANTS)),
|
|
5
|
+
users: new Set(splitList(env.MS365_MCP_OBO_ALLOWED_USERS))
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function readClaims(token) {
|
|
9
|
+
const parts = token.split(".");
|
|
10
|
+
if (parts.length !== 3) return void 0;
|
|
11
|
+
try {
|
|
12
|
+
const claims = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
|
|
13
|
+
return claims && typeof claims === "object" ? claims : void 0;
|
|
14
|
+
} catch {
|
|
15
|
+
return void 0;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function oboAllowlistRefusal(token, allow) {
|
|
19
|
+
if (allow.tenants.size === 0 && allow.users.size === 0) return void 0;
|
|
20
|
+
const claims = readClaims(token);
|
|
21
|
+
if (!claims) return "The access token is not a JWT, so its tenant and user cannot be checked.";
|
|
22
|
+
const tid = typeof claims.tid === "string" ? claims.tid.toLowerCase() : "";
|
|
23
|
+
const oid = typeof claims.oid === "string" ? claims.oid.toLowerCase() : "";
|
|
24
|
+
if (allow.tenants.size > 0 && !allow.tenants.has(tid)) {
|
|
25
|
+
return "The caller's tenant is not allowed on this server.";
|
|
26
|
+
}
|
|
27
|
+
if (allow.users.size > 0 && !allow.users.has(oid)) {
|
|
28
|
+
return "The caller is not allowed on this server.";
|
|
29
|
+
}
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
const ASSERTION_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
33
|
+
"invalid_grant",
|
|
34
|
+
"interaction_required",
|
|
35
|
+
"consent_required",
|
|
36
|
+
"login_required"
|
|
37
|
+
]);
|
|
38
|
+
const ASSERTION_AADSTS = /\bAADSTS(50013|500133|50027|50173|700082|65001|50076|50079)\b/;
|
|
39
|
+
function isRejectedAssertion(error) {
|
|
40
|
+
if (!error || typeof error !== "object") return false;
|
|
41
|
+
const e = error;
|
|
42
|
+
if (typeof e.errorCode === "string" && ASSERTION_ERROR_CODES.has(e.errorCode)) return true;
|
|
43
|
+
const text = `${typeof e.errorMessage === "string" ? e.errorMessage : ""} ${typeof e.message === "string" ? e.message : ""}`;
|
|
44
|
+
return ASSERTION_AADSTS.test(text);
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
isRejectedAssertion,
|
|
48
|
+
oboAllowlistRefusal,
|
|
49
|
+
readOboAllowlist
|
|
50
|
+
};
|
package/dist/server.js
CHANGED
|
@@ -25,6 +25,7 @@ import { ATTACHMENT_ROUTE, loadAttachmentUrlConfig } from "./lib/attachment-url-
|
|
|
25
25
|
import {
|
|
26
26
|
exchangeCodeForToken,
|
|
27
27
|
microsoftBearerTokenAuthMiddleware,
|
|
28
|
+
buildWwwAuthenticate,
|
|
28
29
|
OAuthUpstreamError,
|
|
29
30
|
refreshAccessToken,
|
|
30
31
|
toOAuthErrorResponse
|
|
@@ -35,6 +36,7 @@ import { buildMcpServerInstructions } from "./mcp-instructions.js";
|
|
|
35
36
|
import { installToolSchemaRefNormalization } from "./normalize-tool-schema.js";
|
|
36
37
|
import { MicrosoftOAuthProvider } from "./oauth-provider.js";
|
|
37
38
|
import OboClient from "./obo-client.js";
|
|
39
|
+
import { isRejectedAssertion, oboAllowlistRefusal, readOboAllowlist } from "./lib/obo-access.js";
|
|
38
40
|
import { requestContext } from "./request-context.js";
|
|
39
41
|
import { getSecrets } from "./secrets.js";
|
|
40
42
|
const LOOPBACK_HOSTNAMES = ["localhost", "127.0.0.1", "[::1]"];
|
|
@@ -636,6 +638,12 @@ class MicrosoftGraphServer {
|
|
|
636
638
|
issuerUrl: new URL(publicBase ?? `http://localhost:${port}`)
|
|
637
639
|
})
|
|
638
640
|
);
|
|
641
|
+
const oboAllowlist = this.oboClient ? readOboAllowlist() : null;
|
|
642
|
+
if (oboAllowlist && (oboAllowlist.tenants.size > 0 || oboAllowlist.users.size > 0)) {
|
|
643
|
+
logger.info(
|
|
644
|
+
`OBO allowlist: ${oboAllowlist.tenants.size} tenant(s), ${oboAllowlist.users.size} user(s)`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
639
647
|
const mcpAuth = microsoftBearerTokenAuthMiddleware({
|
|
640
648
|
trustProxyAuth: this.options.trustProxyAuth,
|
|
641
649
|
allowUnauthenticatedDiscovery: this.options.allowUnauthenticatedDiscovery,
|
|
@@ -674,7 +682,23 @@ class MicrosoftGraphServer {
|
|
|
674
682
|
if (req.microsoftAuth) {
|
|
675
683
|
let accessToken = req.microsoftAuth.accessToken;
|
|
676
684
|
if (this.oboClient) {
|
|
677
|
-
|
|
685
|
+
const refusal = oboAllowlist && oboAllowlistRefusal(accessToken, oboAllowlist);
|
|
686
|
+
if (refusal) {
|
|
687
|
+
logger.warn(`OBO caller refused by the allowlist: ${refusal}`);
|
|
688
|
+
res.status(403).json({ error: "access_denied", error_description: refusal });
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
accessToken = await this.oboClient.exchangeToken(accessToken);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
if (!isRejectedAssertion(error)) throw error;
|
|
695
|
+
const description = "The access token was rejected; sign in again";
|
|
696
|
+
res.status(401).set(
|
|
697
|
+
"WWW-Authenticate",
|
|
698
|
+
buildWwwAuthenticate(req, "invalid_token", description, publicBase)
|
|
699
|
+
).json({ error: "invalid_token", error_description: description });
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
678
702
|
}
|
|
679
703
|
await requestContext.run({ accessToken }, handler);
|
|
680
704
|
} else {
|
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.166.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",
|