@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 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 with this
695
- server's own token; the fetcher sends no Authorization header and holds no Microsoft
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
- ### Not available in OAuth/OBO mode
832
+ ### Whose identity the bytes are read as
833
833
 
834
- Identity there arrives per request on the caller's `Authorization` header, and a ticket is
835
- redeemed later by a fetcher that sends none. Minting refuses with an explanation rather
836
- than producing a URL that always fails.
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
 
@@ -78,10 +78,8 @@ function createAttachmentHandler(deps) {
78
78
  }
79
79
  let stream;
80
80
  try {
81
- let accessToken;
82
- if (!deps.authManager.isOAuthModeEnabled()) {
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(
@@ -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
- if (authManager?.isOAuthModeEnabled() || getRequestTokens()) {
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: "Server-minted download URLs are unavailable when Graph identity comes from the request (OAuth, OBO, or bearer mode): the URL is redeemed later without an Authorization header, so the bytes would be fetched as a different identity than the one that asked for them. Use download-bytes."
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 not supported (Graph exposes no pre-authenticated URL for them).'
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
- mint(target, accountName, nowMs = Date.now()) {
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, { target, accountName, expiresAtMs });
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
  *
@@ -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 \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. 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.156.2",
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",