@softeria/ms-365-mcp-server 0.91.0 → 0.93.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/dist/cli.js CHANGED
@@ -35,6 +35,9 @@ program.name("ms-365-mcp-server").description("Microsoft 365 MCP Server").versio
35
35
  ).option(
36
36
  "--public-url <url>",
37
37
  "Public base URL (e.g. https://mcp.example.com) used in browser-facing OAuth redirects when running behind a reverse proxy. Server-to-server endpoints (token, register) stay on the request host."
38
+ ).option(
39
+ "--obo",
40
+ "Enable On-Behalf-Of token exchange in HTTP mode. Exchanges the incoming bearer token for a Graph API token using the OBO flow. Requires MS365_MCP_CLIENT_SECRET."
38
41
  ).addOption(
39
42
  // DEPRECATED: kept only so existing deployments that set --base-url or
40
43
  // MS365_MCP_BASE_URL do not crash at startup. Use --public-url /
@@ -99,6 +102,9 @@ function parseArgs() {
99
102
  options.enableDynamicRegistration = true;
100
103
  }
101
104
  }
105
+ if (process.env.MS365_MCP_OBO === "true" || process.env.MS365_MCP_OBO === "1") {
106
+ options.obo = true;
107
+ }
102
108
  if (options.cloud) {
103
109
  process.env.MS365_MCP_CLOUD_TYPE = options.cloud;
104
110
  }
@@ -521,6 +521,13 @@
521
521
  "scopes": ["Files.Read"],
522
522
  "llmTip": "Generate a short-lived embeddable preview URL for a file (Office docs, PDFs, images). Body: { page?: number | string, zoom?: number, viewer?: 'onedrive' | 'office' }. Returns getUrl (interactive) and postUrl (form-post). Useful for surfacing inline previews in summary emails or chat messages without needing the recipient to open the file."
523
523
  },
524
+ {
525
+ "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/thumbnails",
526
+ "method": "get",
527
+ "toolName": "list-drive-item-thumbnails",
528
+ "scopes": ["Files.Read"],
529
+ "llmTip": "Lists thumbnail sets for a file. Each set contains small (96px), medium (176px), large (800px) thumbnails with url and dimensions. Returns empty for unsupported types (text docs). Use $select=small,medium,large or $expand=small($select=url) to fetch specific sizes. The returned URLs are short-lived — fetch the bytes immediately."
530
+ },
524
531
  {
525
532
  "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/permissions",
526
533
  "method": "get",
@@ -916,6 +916,27 @@ const microsoft_graph_permissionCollectionResponse = z.object({
916
916
  "@odata.nextLink": z.string().nullable(),
917
917
  value: z.array(microsoft_graph_permission)
918
918
  }).partial().passthrough();
919
+ const microsoft_graph_thumbnail = z.object({
920
+ content: z.string().describe("The content stream for the thumbnail.").nullish(),
921
+ height: z.number().gte(-2147483648).lte(2147483647).describe("The height of the thumbnail, in pixels.").nullish(),
922
+ sourceItemId: z.string().describe(
923
+ "The unique identifier of the item that provided the thumbnail. This is only available when a folder thumbnail is requested."
924
+ ).nullish(),
925
+ url: z.string().describe("The URL used to fetch the thumbnail content.").nullish(),
926
+ width: z.number().gte(-2147483648).lte(2147483647).describe("The width of the thumbnail, in pixels.").nullish()
927
+ }).passthrough();
928
+ const microsoft_graph_thumbnailSet = z.object({
929
+ id: z.string().describe("The unique identifier for an entity. Read-only.").optional(),
930
+ large: microsoft_graph_thumbnail.optional(),
931
+ medium: microsoft_graph_thumbnail.optional(),
932
+ small: microsoft_graph_thumbnail.optional(),
933
+ source: microsoft_graph_thumbnail.optional()
934
+ }).passthrough();
935
+ const microsoft_graph_thumbnailSetCollectionResponse = z.object({
936
+ "@odata.count": z.number().int().nullable(),
937
+ "@odata.nextLink": z.string().nullable(),
938
+ value: z.array(microsoft_graph_thumbnailSet)
939
+ }).partial().passthrough();
919
940
  const microsoft_graph_publicationFacet = z.object({
920
941
  checkedOutBy: microsoft_graph_identitySet.optional(),
921
942
  level: z.string().describe(
@@ -4565,6 +4586,9 @@ const schemas = {
4565
4586
  create_drive_item_preview_Body,
4566
4587
  microsoft_graph_itemPreviewInfo,
4567
4588
  microsoft_graph_permissionCollectionResponse,
4589
+ microsoft_graph_thumbnail,
4590
+ microsoft_graph_thumbnailSet,
4591
+ microsoft_graph_thumbnailSetCollectionResponse,
4568
4592
  microsoft_graph_publicationFacet,
4569
4593
  microsoft_graph_driveItemVersion,
4570
4594
  microsoft_graph_driveItemVersionCollectionResponse,
@@ -5769,6 +5793,56 @@ Items with this property set should be removed from your local state.`,
5769
5793
  ],
5770
5794
  response: z.void()
5771
5795
  },
5796
+ {
5797
+ method: "get",
5798
+ path: "/drives/:driveId/items/:driveItemId/thumbnails",
5799
+ alias: "list-drive-item-thumbnails",
5800
+ description: `Collection of thumbnailSet objects associated with the item. For more information, see getting thumbnails. Read-only. Nullable.`,
5801
+ requestFormat: "json",
5802
+ parameters: [
5803
+ {
5804
+ name: "$top",
5805
+ type: "Query",
5806
+ schema: z.number().int().gte(0).describe("Show only the first n items").optional()
5807
+ },
5808
+ {
5809
+ name: "$skip",
5810
+ type: "Query",
5811
+ schema: z.number().int().gte(0).describe("Skip the first n items").optional()
5812
+ },
5813
+ {
5814
+ name: "$search",
5815
+ type: "Query",
5816
+ schema: z.string().describe("Search items by search phrases").optional()
5817
+ },
5818
+ {
5819
+ name: "$filter",
5820
+ type: "Query",
5821
+ schema: z.string().describe("Filter items by property values").optional()
5822
+ },
5823
+ {
5824
+ name: "$count",
5825
+ type: "Query",
5826
+ schema: z.boolean().describe("Include count of items").optional()
5827
+ },
5828
+ {
5829
+ name: "$orderby",
5830
+ type: "Query",
5831
+ schema: z.array(z.string()).describe("Order items by property values").optional()
5832
+ },
5833
+ {
5834
+ name: "$select",
5835
+ type: "Query",
5836
+ schema: z.array(z.string()).describe("Select properties to be returned").optional()
5837
+ },
5838
+ {
5839
+ name: "$expand",
5840
+ type: "Query",
5841
+ schema: z.array(z.string()).describe("Expand related entities").optional()
5842
+ }
5843
+ ],
5844
+ response: z.void()
5845
+ },
5772
5846
  {
5773
5847
  method: "get",
5774
5848
  path: "/drives/:driveId/items/:driveItemId/versions",
@@ -0,0 +1,42 @@
1
+ import { ConfidentialClientApplication } from "@azure/msal-node";
2
+ import logger from "./logger.js";
3
+ import { getCloudEndpoints } from "./cloud-config.js";
4
+ class OboClient {
5
+ constructor(secrets) {
6
+ if (!secrets.clientSecret) {
7
+ throw new Error(
8
+ "On-Behalf-Of flow requires MS365_MCP_CLIENT_SECRET to be set (confidential client)."
9
+ );
10
+ }
11
+ const cloudEndpoints = getCloudEndpoints(secrets.cloudType);
12
+ this.cca = new ConfidentialClientApplication({
13
+ auth: {
14
+ clientId: secrets.clientId,
15
+ clientSecret: secrets.clientSecret,
16
+ authority: `${cloudEndpoints.authority}/${secrets.tenantId || "common"}`
17
+ }
18
+ });
19
+ const graphBase = cloudEndpoints.graphApi.replace(/\/$/, "");
20
+ this.graphScopes = [`${graphBase}/.default`];
21
+ }
22
+ async exchangeToken(userAssertion) {
23
+ try {
24
+ const result = await this.cca.acquireTokenOnBehalfOf({
25
+ oboAssertion: userAssertion,
26
+ scopes: this.graphScopes
27
+ });
28
+ if (!result?.accessToken) {
29
+ throw new Error("OBO token exchange returned no access token");
30
+ }
31
+ logger.info("OBO token exchange successful");
32
+ return result.accessToken;
33
+ } catch (error) {
34
+ logger.error(`OBO token exchange failed: ${error.message}`);
35
+ throw error;
36
+ }
37
+ }
38
+ }
39
+ var obo_client_default = OboClient;
40
+ export {
41
+ obo_client_default as default
42
+ };
package/dist/server.js CHANGED
@@ -19,6 +19,7 @@ import { getSecrets } from "./secrets.js";
19
19
  import { getCloudEndpoints } from "./cloud-config.js";
20
20
  import { requestContext } from "./request-context.js";
21
21
  import crypto from "node:crypto";
22
+ import OboClient from "./obo-client.js";
22
23
  function parseHttpOption(httpOption) {
23
24
  if (typeof httpOption === "boolean") {
24
25
  return { host: void 0, port: 3e3 };
@@ -45,6 +46,7 @@ class MicrosoftGraphServer {
45
46
  this.graphClient = null;
46
47
  this.server = null;
47
48
  this.secrets = null;
49
+ this.oboClient = null;
48
50
  }
49
51
  createMcpServer() {
50
52
  const server = new McpServer(
@@ -103,6 +105,18 @@ class MicrosoftGraphServer {
103
105
  } catch (err) {
104
106
  logger.warn(`Failed to detect multi-account mode: ${err.message}`);
105
107
  }
108
+ if (this.options.obo) {
109
+ if (!this.options.http) {
110
+ throw new Error("--obo requires --http (On-Behalf-Of flow only works in HTTP mode).");
111
+ }
112
+ if (!this.secrets.clientSecret) {
113
+ throw new Error(
114
+ "--obo requires MS365_MCP_CLIENT_SECRET to be set (confidential client required for On-Behalf-Of flow)."
115
+ );
116
+ }
117
+ this.oboClient = new OboClient(this.secrets);
118
+ logger.info("On-Behalf-Of (OBO) flow enabled");
119
+ }
106
120
  const outputFormat = this.options.toon ? "toon" : "json";
107
121
  this.graphClient = new GraphClient(this.authManager, this.secrets, outputFormat);
108
122
  if (!this.options.http) {
@@ -174,7 +188,7 @@ class MicrosoftGraphServer {
174
188
  const protocol = req.secure ? "https" : "http";
175
189
  const requestOrigin = `${protocol}://${req.get("host")}`;
176
190
  const browserBase = publicBase ?? requestOrigin;
177
- const scopes = buildScopesFromEndpoints(this.options.orgMode, this.options.enabledTools);
191
+ const scopes = this.options.obo ? [`api://${this.secrets.clientId}/access_as_user`] : buildScopesFromEndpoints(this.options.orgMode, this.options.enabledTools);
178
192
  res.json({
179
193
  resource: `${requestOrigin}/mcp`,
180
194
  authorization_servers: [browserBase],
@@ -398,7 +412,11 @@ class MicrosoftGraphServer {
398
412
  };
399
413
  try {
400
414
  if (req.microsoftAuth) {
401
- await requestContext.run({ accessToken: req.microsoftAuth.accessToken }, handler);
415
+ let accessToken = req.microsoftAuth.accessToken;
416
+ if (this.oboClient) {
417
+ accessToken = await this.oboClient.exchangeToken(accessToken);
418
+ }
419
+ await requestContext.run({ accessToken }, handler);
402
420
  } else {
403
421
  await handler();
404
422
  }
@@ -436,7 +454,11 @@ class MicrosoftGraphServer {
436
454
  };
437
455
  try {
438
456
  if (req.microsoftAuth) {
439
- await requestContext.run({ accessToken: req.microsoftAuth.accessToken }, handler);
457
+ let accessToken = req.microsoftAuth.accessToken;
458
+ if (this.oboClient) {
459
+ accessToken = await this.oboClient.exchangeToken(accessToken);
460
+ }
461
+ await requestContext.run({ accessToken }, handler);
440
462
  } else {
441
463
  await handler();
442
464
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.91.0",
3
+ "version": "0.93.0",
4
4
  "description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -521,6 +521,13 @@
521
521
  "scopes": ["Files.Read"],
522
522
  "llmTip": "Generate a short-lived embeddable preview URL for a file (Office docs, PDFs, images). Body: { page?: number | string, zoom?: number, viewer?: 'onedrive' | 'office' }. Returns getUrl (interactive) and postUrl (form-post). Useful for surfacing inline previews in summary emails or chat messages without needing the recipient to open the file."
523
523
  },
524
+ {
525
+ "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/thumbnails",
526
+ "method": "get",
527
+ "toolName": "list-drive-item-thumbnails",
528
+ "scopes": ["Files.Read"],
529
+ "llmTip": "Lists thumbnail sets for a file. Each set contains small (96px), medium (176px), large (800px) thumbnails with url and dimensions. Returns empty for unsupported types (text docs). Use $select=small,medium,large or $expand=small($select=url) to fetch specific sizes. The returned URLs are short-lived — fetch the bytes immediately."
530
+ },
524
531
  {
525
532
  "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/permissions",
526
533
  "method": "get",