@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 +6 -0
- package/dist/endpoints.json +7 -0
- package/dist/generated/client.js +74 -0
- package/dist/obo-client.js +42 -0
- package/dist/server.js +25 -3
- package/package.json +1 -1
- package/src/endpoints.json +7 -0
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
|
}
|
package/dist/endpoints.json
CHANGED
|
@@ -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",
|
package/dist/generated/client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
package/src/endpoints.json
CHANGED
|
@@ -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",
|