@softeria/ms-365-mcp-server 0.156.2 → 0.157.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 +21 -6
- package/dist/attachment-route.js +2 -4
- package/dist/graph-tools.js +6 -4
- package/dist/lib/attachment-tickets.js +11 -2
- package/dist/mcp-instructions.js +1 -1
- package/dist/server.js +0 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -691,8 +691,8 @@ GET /attachment?t=<ticket>&dgk=<key-id>&dgx=<expiry>&dgs=<signature>
|
|
|
691
691
|
```
|
|
692
692
|
|
|
693
693
|
The ticket is 32 bytes of CSPRNG output, **single-use**, memory-only, and expires after
|
|
694
|
-
`MS365_MCP_ATTACHMENT_URL_TTL_S` seconds. Redeeming it streams the Graph bytes
|
|
695
|
-
|
|
694
|
+
`MS365_MCP_ATTACHMENT_URL_TTL_S` seconds. Redeeming it streams the Graph bytes as the
|
|
695
|
+
identity that minted it; the fetcher sends no Authorization header and holds no Microsoft
|
|
696
696
|
credential.
|
|
697
697
|
|
|
698
698
|
**This grants no authority the calling agent did not already have.** Every target that can
|
|
@@ -829,11 +829,26 @@ exists rather than being assumed.
|
|
|
829
829
|
The ticket travels in the **query, not the path**, because the verifying sidecar keeps a
|
|
830
830
|
fetched URL's path in its error messages and strips the query.
|
|
831
831
|
|
|
832
|
-
###
|
|
832
|
+
### Whose identity the bytes are read as
|
|
833
833
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
834
|
+
Under `--trust-proxy-auth` the server reads with its own cached account, and redemption
|
|
835
|
+
looks that account up again.
|
|
836
|
+
|
|
837
|
+
In plain `--http` and `--obo`, identity arrives per request on the caller's
|
|
838
|
+
`Authorization` header, and a ticket is redeemed later by a fetcher that sends none. So
|
|
839
|
+
the ticket keeps the Graph token the minting request used (the exchanged one, under
|
|
840
|
+
`--obo`) and redemption reads with that token and nothing else. It stays in server memory
|
|
841
|
+
and is never part of the URL.
|
|
842
|
+
|
|
843
|
+
With `MS365_MCP_OAUTH_TOKEN` set, the ticket keeps that token instead, `--trust-proxy-auth`
|
|
844
|
+
or not.
|
|
845
|
+
|
|
846
|
+
A token kept this way cannot be refreshed. If it expires before the URL is fetched,
|
|
847
|
+
redemption answers 502. The agent can mint again once its client has a fresh token; an
|
|
848
|
+
expired `MS365_MCP_OAUTH_TOKEN` has to be replaced by the operator.
|
|
849
|
+
|
|
850
|
+
Tickets live in the memory of the process that minted them, so a URL has to be redeemed
|
|
851
|
+
on the same instance. Behind a load balancer that means one replica or sticky routing.
|
|
837
852
|
|
|
838
853
|
## Token Storage
|
|
839
854
|
|
package/dist/attachment-route.js
CHANGED
|
@@ -78,10 +78,8 @@ function createAttachmentHandler(deps) {
|
|
|
78
78
|
}
|
|
79
79
|
let stream;
|
|
80
80
|
try {
|
|
81
|
-
|
|
82
|
-
if (!
|
|
83
|
-
accessToken = await deps.authManager.getTokenForAccount(ticket.accountName);
|
|
84
|
-
}
|
|
81
|
+
const accessToken = ticket.kind === "request-token" ? ticket.accessToken : await deps.authManager.getTokenForAccount(ticket.accountName);
|
|
82
|
+
if (!accessToken) throw new Error("No access token for this ticket");
|
|
85
83
|
stream = await graphClient.downloadStream(ticket.target, { accessToken });
|
|
86
84
|
} catch (error) {
|
|
87
85
|
logger.error(
|
package/dist/graph-tools.js
CHANGED
|
@@ -529,13 +529,15 @@ async function checkAccountParamInBearerMode(accountParam, authManager) {
|
|
|
529
529
|
async function mintDownloadUrl(target, accountParam, authManager) {
|
|
530
530
|
const minting = getAttachmentMinting();
|
|
531
531
|
if (!minting) return null;
|
|
532
|
-
|
|
532
|
+
const identityFromRequest = Boolean(authManager?.isOAuthModeEnabled() || getRequestTokens());
|
|
533
|
+
const requestToken = identityFromRequest ? getRequestTokens()?.accessToken ?? await authManager?.getToken().catch(() => null) ?? void 0 : void 0;
|
|
534
|
+
if (identityFromRequest && !requestToken) {
|
|
533
535
|
return {
|
|
534
536
|
content: [
|
|
535
537
|
{
|
|
536
538
|
type: "text",
|
|
537
539
|
text: JSON.stringify({
|
|
538
|
-
error: "
|
|
540
|
+
error: "No Graph access token is available for this request, so no download URL can be minted. Use download-bytes."
|
|
539
541
|
})
|
|
540
542
|
}
|
|
541
543
|
],
|
|
@@ -564,7 +566,7 @@ async function mintDownloadUrl(target, accountParam, authManager) {
|
|
|
564
566
|
}
|
|
565
567
|
let ticket;
|
|
566
568
|
try {
|
|
567
|
-
ticket = minting.store.mint(target, accountParam);
|
|
569
|
+
ticket = requestToken ? minting.store.mintWithToken(target, requestToken) : minting.store.mint(target, accountParam);
|
|
568
570
|
} catch (error) {
|
|
569
571
|
if (error instanceof TicketStoreFullError) {
|
|
570
572
|
return {
|
|
@@ -846,7 +848,7 @@ const UTILITY_TOOLS = [
|
|
|
846
848
|
buildSchema: (ctx) => {
|
|
847
849
|
const schema = {
|
|
848
850
|
target: z.string().describe(
|
|
849
|
-
'Relative Microsoft Graph path starting with "/". Either a driveItem content path or the item path itself, e.g. /drives/{drive-id}/items/{driveItem-id}/content, /me/drive/items/{driveItem-id}/content, or /sites/{site-id}/drive/items/{driveItem-id}. A trailing /content is optional and is stripped automatically for drive items. Mail attachment $value paths and meeting recordings are
|
|
851
|
+
'Relative Microsoft Graph path starting with "/". Either a driveItem content path or the item path itself, e.g. /drives/{drive-id}/items/{driveItem-id}/content, /me/drive/items/{driveItem-id}/content, or /sites/{site-id}/drive/items/{driveItem-id}. A trailing /content is optional and is stripped automatically for drive items. Mail attachment $value paths and meeting recordings are accepted on a server running with --enable-attachment-urls.'
|
|
850
852
|
)
|
|
851
853
|
};
|
|
852
854
|
if (ctx.multiAccount) {
|
|
@@ -44,16 +44,25 @@ class AttachmentTicketStore {
|
|
|
44
44
|
if (ticket.expiresAtMs <= nowMs) this.tickets.delete(id);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
add(target, identity, nowMs) {
|
|
48
48
|
this.sweep(nowMs);
|
|
49
49
|
if (this.tickets.size >= MAX_LIVE_TICKETS) {
|
|
50
50
|
throw new TicketStoreFullError(MAX_LIVE_TICKETS);
|
|
51
51
|
}
|
|
52
52
|
const id = randomBytes(TICKET_BYTES).toString("base64url");
|
|
53
53
|
const expiresAtMs = nowMs + this.ttlSeconds * 1e3;
|
|
54
|
-
this.tickets.set(id, {
|
|
54
|
+
this.tickets.set(id, { ...identity, target, expiresAtMs });
|
|
55
55
|
return { id, expiresAtMs };
|
|
56
56
|
}
|
|
57
|
+
/** Mint a ticket redeemed with this server's own token for `accountName`. */
|
|
58
|
+
mint(target, accountName, nowMs = Date.now()) {
|
|
59
|
+
return this.add(target, { kind: "server-account", accountName }, nowMs);
|
|
60
|
+
}
|
|
61
|
+
/** Mint a ticket redeemed with `accessToken` and nothing else. */
|
|
62
|
+
mintWithToken(target, accessToken, nowMs = Date.now()) {
|
|
63
|
+
if (!accessToken) throw new Error("A request-token ticket needs a token");
|
|
64
|
+
return this.add(target, { kind: "request-token", accessToken }, nowMs);
|
|
65
|
+
}
|
|
57
66
|
/**
|
|
58
67
|
* Return the ticket and burn it, or undefined.
|
|
59
68
|
*
|
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 (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
|
|
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. For large mail attachments and meeting recordings that is one out-of-band option; get-download-url is the other, on a server running with --enable-attachment-urls. 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
|
@@ -692,12 +692,6 @@ class MicrosoftGraphServer {
|
|
|
692
692
|
}
|
|
693
693
|
);
|
|
694
694
|
const attachmentConfig = loadAttachmentUrlConfig(Boolean(this.options.enableAttachmentUrls));
|
|
695
|
-
const mintingAlwaysRefused = this.authManager?.isOAuthModeEnabled() === true || !this.options.trustProxyAuth;
|
|
696
|
-
if (attachmentConfig && mintingAlwaysRefused) {
|
|
697
|
-
logger.warn(
|
|
698
|
-
"--enable-attachment-urls is on, but this server takes its Graph identity from the request in plain --http mode, and minting is refused whenever it does (the URL is redeemed later with no Authorization header, so the bytes would be fetched as a different identity). get-download-url will keep answering with download-bytes. Server-minted URLs need --trust-proxy-auth, where the server uses its own token."
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
695
|
let attachmentApp = null;
|
|
702
696
|
if (attachmentConfig) {
|
|
703
697
|
const ticketStore = new AttachmentTicketStore(attachmentConfig.ttlSeconds);
|
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.157.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",
|