@softeria/ms-365-mcp-server 0.29.0 → 0.30.1
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/.env.example +11 -1
- package/README.md +61 -2
- package/dist/auth.js +7 -3
- package/dist/cli.js +4 -1
- package/dist/cloud-config.js +49 -0
- package/dist/generated/client.js +13 -12
- package/dist/graph-client.js +20 -19
- package/dist/lib/microsoft-auth.js +7 -4
- package/dist/oauth-provider.js +6 -4
- package/dist/request-context.js +9 -0
- package/dist/secrets.js +13 -7
- package/dist/server.js +36 -17
- package/logs/mcp-server.log +5 -5
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -10,7 +10,10 @@ MS365_MCP_CLIENT_SECRET=your-azure-ad-app-client-secret-here
|
|
|
10
10
|
# Tenant ID - use "common" for multi-tenant or your specific tenant ID
|
|
11
11
|
MS365_MCP_TENANT_ID=common
|
|
12
12
|
|
|
13
|
-
#
|
|
13
|
+
# Cloud environment: global (default) or china (21Vianet)
|
|
14
|
+
# MS365_MCP_CLOUD_TYPE=global
|
|
15
|
+
|
|
16
|
+
# Instructions for Global Cloud:
|
|
14
17
|
# 1. Go to https://portal.azure.com
|
|
15
18
|
# 2. Navigate to Azure Active Directory → App registrations → New registration
|
|
16
19
|
# 3. Set name: "MS365 MCP Server"
|
|
@@ -23,6 +26,12 @@ MS365_MCP_TENANT_ID=common
|
|
|
23
26
|
# 7. Replace the values above with your actual credentials
|
|
24
27
|
# 8. Rename this file to .env
|
|
25
28
|
|
|
29
|
+
# Instructions for China Cloud (21Vianet):
|
|
30
|
+
# 1. Go to https://portal.azure.cn
|
|
31
|
+
# 2. Navigate to Azure Active Directory → App registrations → New registration
|
|
32
|
+
# 3. Follow the same steps as above
|
|
33
|
+
# 4. Set MS365_MCP_CLOUD_TYPE=china
|
|
34
|
+
|
|
26
35
|
# -------------------------------------------------------------------
|
|
27
36
|
# Azure Key Vault Integration (Optional)
|
|
28
37
|
# -------------------------------------------------------------------
|
|
@@ -35,6 +44,7 @@ MS365_MCP_TENANT_ID=common
|
|
|
35
44
|
# - ms365-mcp-client-id (required)
|
|
36
45
|
# - ms365-mcp-tenant-id (optional, defaults to "common")
|
|
37
46
|
# - ms365-mcp-client-secret (optional)
|
|
47
|
+
# - ms365-mcp-cloud-type (optional, defaults to "global")
|
|
38
48
|
#
|
|
39
49
|
# Authentication uses DefaultAzureCredential, which supports:
|
|
40
50
|
# - Managed Identity (recommended for Azure Container Apps)
|
package/README.md
CHANGED
|
@@ -7,6 +7,15 @@ Microsoft 365 MCP Server
|
|
|
7
7
|
A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Microsoft Office services through the Graph
|
|
8
8
|
API.
|
|
9
9
|
|
|
10
|
+
## Supported Clouds
|
|
11
|
+
|
|
12
|
+
This server supports multiple Microsoft cloud environments:
|
|
13
|
+
|
|
14
|
+
| Cloud | Description | Auth Endpoint | Graph API Endpoint |
|
|
15
|
+
| -------------------- | ---------------------------------- | ------------------------- | ------------------------------- |
|
|
16
|
+
| **Global** (default) | International Microsoft 365 | login.microsoftonline.com | graph.microsoft.com |
|
|
17
|
+
| **China** (21Vianet) | Microsoft 365 operated by 21Vianet | login.chinacloudapi.cn | microsoftgraph.chinacloudapi.cn |
|
|
18
|
+
|
|
10
19
|
## Prerequisites
|
|
11
20
|
|
|
12
21
|
- Node.js >= 20 (recommended)
|
|
@@ -188,9 +197,9 @@ Test login in Claude Desktop:
|
|
|
188
197
|
|
|
189
198
|
### Claude Desktop
|
|
190
199
|
|
|
191
|
-
To add this MCP server to Claude Desktop
|
|
200
|
+
To add this MCP server to Claude Desktop, edit the config file under Settings > Developer.
|
|
192
201
|
|
|
193
|
-
|
|
202
|
+
#### Personal Account (MSA)
|
|
194
203
|
|
|
195
204
|
```json
|
|
196
205
|
{
|
|
@@ -203,12 +212,60 @@ Edit the config file under Settings > Developer:
|
|
|
203
212
|
}
|
|
204
213
|
```
|
|
205
214
|
|
|
215
|
+
#### Work/School Account (Global)
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"mcpServers": {
|
|
220
|
+
"ms365": {
|
|
221
|
+
"command": "npx",
|
|
222
|
+
"args": ["-y", "@softeria/ms-365-mcp-server", "--org-mode"]
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### Work/School Account (China 21Vianet)
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"mcpServers": {
|
|
233
|
+
"ms365-china": {
|
|
234
|
+
"command": "npx",
|
|
235
|
+
"args": ["-y", "@softeria/ms-365-mcp-server", "--org-mode", "--cloud", "china"]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
206
241
|
### Claude Code CLI
|
|
207
242
|
|
|
243
|
+
#### Personal Account (MSA)
|
|
244
|
+
|
|
208
245
|
```bash
|
|
209
246
|
claude mcp add ms365 -- npx -y @softeria/ms-365-mcp-server
|
|
210
247
|
```
|
|
211
248
|
|
|
249
|
+
#### Work/School Account (Global)
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# macOS/Linux
|
|
253
|
+
claude mcp add ms365 -- npx -y @softeria/ms-365-mcp-server --org-mode
|
|
254
|
+
|
|
255
|
+
# Windows (use cmd /c wrapper)
|
|
256
|
+
claude mcp add ms365 -s user -- cmd /c "npx -y @softeria/ms-365-mcp-server --org-mode"
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### Work/School Account (China 21Vianet)
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
# macOS/Linux
|
|
263
|
+
claude mcp add ms365-china -- npx -y @softeria/ms-365-mcp-server --org-mode --cloud china
|
|
264
|
+
|
|
265
|
+
# Windows (use cmd /c wrapper)
|
|
266
|
+
claude mcp add ms365-china -s user -- cmd /c "npx -y @softeria/ms-365-mcp-server --org-mode --cloud china"
|
|
267
|
+
```
|
|
268
|
+
|
|
212
269
|
For other interfaces that support MCPs, please refer to their respective documentation for the correct
|
|
213
270
|
integration method.
|
|
214
271
|
|
|
@@ -364,6 +421,7 @@ The following options can be used when running ms-365-mcp-server directly from t
|
|
|
364
421
|
--org-mode Enable organization/work mode from start (includes Teams, SharePoint, etc.)
|
|
365
422
|
--work-mode Alias for --org-mode
|
|
366
423
|
--force-work-scopes Backwards compatibility alias for --org-mode (deprecated)
|
|
424
|
+
--cloud <type> Microsoft cloud environment: global (default) or china (21Vianet)
|
|
367
425
|
```
|
|
368
426
|
|
|
369
427
|
### Server Options
|
|
@@ -390,6 +448,7 @@ Environment variables:
|
|
|
390
448
|
- `MS365_MCP_ORG_MODE=true|1`: Enable organization/work mode (alternative to --org-mode flag)
|
|
391
449
|
- `MS365_MCP_FORCE_WORK_SCOPES=true|1`: Backwards compatibility for MS365_MCP_ORG_MODE
|
|
392
450
|
- `MS365_MCP_OUTPUT_FORMAT=toon`: Enable TOON output format (alternative to --toon flag)
|
|
451
|
+
- `MS365_MCP_CLOUD_TYPE=global|china`: Microsoft cloud environment (alternative to --cloud flag)
|
|
393
452
|
- `LOG_LEVEL`: Set logging level (default: 'info')
|
|
394
453
|
- `SILENT=true|1`: Disable console output
|
|
395
454
|
- `MS365_MCP_CLIENT_ID`: Custom Azure app client ID (defaults to built-in app)
|
package/dist/auth.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs, { existsSync, readFileSync } from "fs";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { getSecrets } from "./secrets.js";
|
|
7
|
+
import { getCloudEndpoints, getDefaultClientId } from "./cloud-config.js";
|
|
7
8
|
let keytar = null;
|
|
8
9
|
async function getKeytar() {
|
|
9
10
|
if (keytar === void 0) {
|
|
@@ -36,10 +37,11 @@ const FALLBACK_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
36
37
|
const FALLBACK_PATH = path.join(FALLBACK_DIR, "..", ".token-cache.json");
|
|
37
38
|
const SELECTED_ACCOUNT_PATH = path.join(FALLBACK_DIR, "..", ".selected-account.json");
|
|
38
39
|
function createMsalConfig(secrets) {
|
|
40
|
+
const cloudEndpoints = getCloudEndpoints(secrets.cloudType);
|
|
39
41
|
return {
|
|
40
42
|
auth: {
|
|
41
|
-
clientId: secrets.clientId ||
|
|
42
|
-
authority:
|
|
43
|
+
clientId: secrets.clientId || getDefaultClientId(secrets.cloudType),
|
|
44
|
+
authority: `${cloudEndpoints.authority}/${secrets.tenantId || "common"}`
|
|
43
45
|
}
|
|
44
46
|
};
|
|
45
47
|
}
|
|
@@ -299,7 +301,9 @@ class AuthManager {
|
|
|
299
301
|
}
|
|
300
302
|
logger.info("Token retrieved successfully, testing Graph API access...");
|
|
301
303
|
try {
|
|
302
|
-
const
|
|
304
|
+
const secrets = await getSecrets();
|
|
305
|
+
const cloudEndpoints = getCloudEndpoints(secrets.cloudType);
|
|
306
|
+
const response = await fetch(`${cloudEndpoints.graphApi}/v1.0/me`, {
|
|
303
307
|
headers: {
|
|
304
308
|
Authorization: `Bearer ${token}`
|
|
305
309
|
}
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ program.name("ms-365-mcp-server").description("Microsoft 365 MCP Server").versio
|
|
|
23
23
|
).option("--list-presets", "List all available presets and exit").option(
|
|
24
24
|
"--org-mode",
|
|
25
25
|
"Enable organization/work mode from start (includes Teams, SharePoint, etc.)"
|
|
26
|
-
).option("--work-mode", "Alias for --org-mode").option("--force-work-scopes", "Backwards compatibility alias for --org-mode (deprecated)").option("--toon", "(experimental) Enable TOON output format for 30-60% token reduction").option("--discovery", "Enable runtime tool discovery and loading (experimental feature)");
|
|
26
|
+
).option("--work-mode", "Alias for --org-mode").option("--force-work-scopes", "Backwards compatibility alias for --org-mode (deprecated)").option("--toon", "(experimental) Enable TOON output format for 30-60% token reduction").option("--discovery", "Enable runtime tool discovery and loading (experimental feature)").option("--cloud <type>", "Microsoft cloud environment: global (default) or china (21Vianet)");
|
|
27
27
|
function parseArgs() {
|
|
28
28
|
program.parse();
|
|
29
29
|
const options = program.opts();
|
|
@@ -65,6 +65,9 @@ function parseArgs() {
|
|
|
65
65
|
if (process.env.MS365_MCP_OUTPUT_FORMAT === "toon") {
|
|
66
66
|
options.toon = true;
|
|
67
67
|
}
|
|
68
|
+
if (options.cloud) {
|
|
69
|
+
process.env.MS365_MCP_CLOUD_TYPE = options.cloud;
|
|
70
|
+
}
|
|
68
71
|
return options;
|
|
69
72
|
}
|
|
70
73
|
export {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const CLOUD_ENDPOINTS = {
|
|
2
|
+
global: {
|
|
3
|
+
authority: "https://login.microsoftonline.com",
|
|
4
|
+
graphApi: "https://graph.microsoft.com",
|
|
5
|
+
portal: "https://portal.azure.com"
|
|
6
|
+
},
|
|
7
|
+
china: {
|
|
8
|
+
authority: "https://login.chinacloudapi.cn",
|
|
9
|
+
graphApi: "https://microsoftgraph.chinacloudapi.cn",
|
|
10
|
+
portal: "https://portal.azure.cn"
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const DEFAULT_CLIENT_IDS = {
|
|
14
|
+
global: "084a3e9f-a9f4-43f7-89f9-d229cf97853e",
|
|
15
|
+
china: "f3e61a6e-bc26-4281-8588-2c7359a02141"
|
|
16
|
+
};
|
|
17
|
+
function getDefaultClientId(cloudType = "global") {
|
|
18
|
+
return DEFAULT_CLIENT_IDS[cloudType];
|
|
19
|
+
}
|
|
20
|
+
function getCloudEndpoints(cloudType = "global") {
|
|
21
|
+
const endpoints = CLOUD_ENDPOINTS[cloudType];
|
|
22
|
+
if (!endpoints) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Unknown cloud type: ${cloudType}. Valid values: ${Object.keys(CLOUD_ENDPOINTS).join(", ")}`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return endpoints;
|
|
28
|
+
}
|
|
29
|
+
function isValidCloudType(value) {
|
|
30
|
+
return value in CLOUD_ENDPOINTS;
|
|
31
|
+
}
|
|
32
|
+
function parseCloudType(value) {
|
|
33
|
+
if (!value) return "global";
|
|
34
|
+
const normalized = value.toLowerCase().trim();
|
|
35
|
+
if (!isValidCloudType(normalized)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Invalid cloud type: ${value}. Valid values: ${Object.keys(CLOUD_ENDPOINTS).join(", ")}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
CLOUD_ENDPOINTS,
|
|
44
|
+
DEFAULT_CLIENT_IDS,
|
|
45
|
+
getCloudEndpoints,
|
|
46
|
+
getDefaultClientId,
|
|
47
|
+
isValidCloudType,
|
|
48
|
+
parseCloudType
|
|
49
|
+
};
|
package/dist/generated/client.js
CHANGED
|
@@ -662,8 +662,8 @@ const microsoft_graph_driveItem = z.object({
|
|
|
662
662
|
webUrl: z.string().describe(
|
|
663
663
|
"URL that either displays the resource in the browser (for Office file formats), or is a direct link to the file (for other formats). Read-only."
|
|
664
664
|
).nullish(),
|
|
665
|
-
createdByUser: microsoft_graph_user.describe("[Note: Simplified from
|
|
666
|
-
lastModifiedByUser: microsoft_graph_user.describe("[Note: Simplified from
|
|
665
|
+
createdByUser: microsoft_graph_user.describe("[Note: Simplified from 135 properties to 25 most common ones]").optional(),
|
|
666
|
+
lastModifiedByUser: microsoft_graph_user.describe("[Note: Simplified from 135 properties to 25 most common ones]").optional(),
|
|
667
667
|
audio: microsoft_graph_audio.optional(),
|
|
668
668
|
bundle: microsoft_graph_bundle.optional(),
|
|
669
669
|
cTag: z.string().describe(
|
|
@@ -1759,9 +1759,10 @@ const microsoft_graph_listItem = z.object({
|
|
|
1759
1759
|
webUrl: z.string().describe(
|
|
1760
1760
|
"URL that either displays the resource in the browser (for Office file formats), or is a direct link to the file (for other formats). Read-only."
|
|
1761
1761
|
).nullish(),
|
|
1762
|
-
createdByUser: microsoft_graph_user.describe("[Note: Simplified from
|
|
1763
|
-
lastModifiedByUser: microsoft_graph_user.describe("[Note: Simplified from
|
|
1762
|
+
createdByUser: microsoft_graph_user.describe("[Note: Simplified from 135 properties to 25 most common ones]").optional(),
|
|
1763
|
+
lastModifiedByUser: microsoft_graph_user.describe("[Note: Simplified from 135 properties to 25 most common ones]").optional(),
|
|
1764
1764
|
contentType: microsoft_graph_contentTypeInfo.optional(),
|
|
1765
|
+
deleted: microsoft_graph_deleted.optional(),
|
|
1765
1766
|
sharepointIds: microsoft_graph_sharepointIds.optional(),
|
|
1766
1767
|
analytics: microsoft_graph_itemAnalytics.optional(),
|
|
1767
1768
|
documentSetVersions: z.array(microsoft_graph_documentSetVersion).describe("Version information for a document set version created by a user.").optional(),
|
|
@@ -1879,8 +1880,8 @@ const microsoft_graph_list = z.lazy(
|
|
|
1879
1880
|
webUrl: z.string().describe(
|
|
1880
1881
|
"URL that either displays the resource in the browser (for Office file formats), or is a direct link to the file (for other formats). Read-only."
|
|
1881
1882
|
).nullish(),
|
|
1882
|
-
createdByUser: microsoft_graph_user.describe("[Note: Simplified from
|
|
1883
|
-
lastModifiedByUser: microsoft_graph_user.describe("[Note: Simplified from
|
|
1883
|
+
createdByUser: microsoft_graph_user.describe("[Note: Simplified from 135 properties to 25 most common ones]").optional(),
|
|
1884
|
+
lastModifiedByUser: microsoft_graph_user.describe("[Note: Simplified from 135 properties to 25 most common ones]").optional(),
|
|
1884
1885
|
displayName: z.string().describe("The displayable title of the list.").nullish(),
|
|
1885
1886
|
list: microsoft_graph_listInfo.optional(),
|
|
1886
1887
|
sharepointIds: microsoft_graph_sharepointIds.optional(),
|
|
@@ -1911,8 +1912,8 @@ const microsoft_graph_drive = z.lazy(
|
|
|
1911
1912
|
webUrl: z.string().describe(
|
|
1912
1913
|
"URL that either displays the resource in the browser (for Office file formats), or is a direct link to the file (for other formats). Read-only."
|
|
1913
1914
|
).nullish(),
|
|
1914
|
-
createdByUser: microsoft_graph_user.describe("[Note: Simplified from
|
|
1915
|
-
lastModifiedByUser: microsoft_graph_user.describe("[Note: Simplified from
|
|
1915
|
+
createdByUser: microsoft_graph_user.describe("[Note: Simplified from 135 properties to 25 most common ones]").optional(),
|
|
1916
|
+
lastModifiedByUser: microsoft_graph_user.describe("[Note: Simplified from 135 properties to 25 most common ones]").optional(),
|
|
1916
1917
|
driveType: z.string().describe(
|
|
1917
1918
|
"Describes the type of drive represented by this resource. OneDrive personal drives return personal. OneDrive for Business returns business. SharePoint document libraries return documentLibrary. Read-only."
|
|
1918
1919
|
).nullish(),
|
|
@@ -3166,8 +3167,8 @@ const microsoft_graph_baseItem = z.object({
|
|
|
3166
3167
|
webUrl: z.string().describe(
|
|
3167
3168
|
"URL that either displays the resource in the browser (for Office file formats), or is a direct link to the file (for other formats). Read-only."
|
|
3168
3169
|
).nullish(),
|
|
3169
|
-
createdByUser: microsoft_graph_user.describe("[Note: Simplified from
|
|
3170
|
-
lastModifiedByUser: microsoft_graph_user.describe("[Note: Simplified from
|
|
3170
|
+
createdByUser: microsoft_graph_user.describe("[Note: Simplified from 135 properties to 25 most common ones]").optional(),
|
|
3171
|
+
lastModifiedByUser: microsoft_graph_user.describe("[Note: Simplified from 135 properties to 25 most common ones]").optional()
|
|
3171
3172
|
}).strict();
|
|
3172
3173
|
const microsoft_graph_site = z.object({
|
|
3173
3174
|
id: z.string().describe("The unique identifier for an entity. Read-only.").optional(),
|
|
@@ -3188,8 +3189,8 @@ const microsoft_graph_site = z.object({
|
|
|
3188
3189
|
webUrl: z.string().describe(
|
|
3189
3190
|
"URL that either displays the resource in the browser (for Office file formats), or is a direct link to the file (for other formats). Read-only."
|
|
3190
3191
|
).nullish(),
|
|
3191
|
-
createdByUser: microsoft_graph_user.describe("[Note: Simplified from
|
|
3192
|
-
lastModifiedByUser: microsoft_graph_user.describe("[Note: Simplified from
|
|
3192
|
+
createdByUser: microsoft_graph_user.describe("[Note: Simplified from 135 properties to 25 most common ones]").optional(),
|
|
3193
|
+
lastModifiedByUser: microsoft_graph_user.describe("[Note: Simplified from 135 properties to 25 most common ones]").optional(),
|
|
3193
3194
|
isPersonalSite: z.boolean().describe("Identifies whether the site is personal or not. Read-only.").nullish(),
|
|
3194
3195
|
root: microsoft_graph_root.optional(),
|
|
3195
3196
|
sharepointIds: microsoft_graph_sharepointIds.optional(),
|
package/dist/graph-client.js
CHANGED
|
@@ -1,33 +1,27 @@
|
|
|
1
1
|
import logger from "./logger.js";
|
|
2
2
|
import { refreshAccessToken } from "./lib/microsoft-auth.js";
|
|
3
3
|
import { encode as toonEncode } from "@toon-format/toon";
|
|
4
|
+
import { getCloudEndpoints } from "./cloud-config.js";
|
|
5
|
+
import { getRequestTokens } from "./request-context.js";
|
|
4
6
|
class GraphClient {
|
|
5
7
|
constructor(authManager, secrets, outputFormat = "json") {
|
|
6
|
-
this.accessToken = null;
|
|
7
|
-
this.refreshToken = null;
|
|
8
8
|
this.outputFormat = "json";
|
|
9
9
|
this.authManager = authManager;
|
|
10
10
|
this.secrets = secrets;
|
|
11
11
|
this.outputFormat = outputFormat;
|
|
12
12
|
}
|
|
13
|
-
setOAuthTokens(accessToken, refreshToken) {
|
|
14
|
-
this.accessToken = accessToken;
|
|
15
|
-
this.refreshToken = refreshToken || null;
|
|
16
|
-
}
|
|
17
13
|
async makeRequest(endpoint, options = {}) {
|
|
18
|
-
|
|
19
|
-
let
|
|
14
|
+
const contextTokens = getRequestTokens();
|
|
15
|
+
let accessToken = options.accessToken ?? contextTokens?.accessToken ?? await this.authManager.getToken();
|
|
16
|
+
const refreshToken = options.refreshToken ?? contextTokens?.refreshToken;
|
|
20
17
|
if (!accessToken) {
|
|
21
18
|
throw new Error("No access token available");
|
|
22
19
|
}
|
|
23
20
|
try {
|
|
24
21
|
let response = await this.performRequest(endpoint, accessToken, options);
|
|
25
22
|
if (response.status === 401 && refreshToken) {
|
|
26
|
-
await this.refreshAccessToken(refreshToken);
|
|
27
|
-
accessToken =
|
|
28
|
-
if (!accessToken) {
|
|
29
|
-
throw new Error("Failed to refresh access token");
|
|
30
|
-
}
|
|
23
|
+
const newTokens = await this.refreshAccessToken(refreshToken);
|
|
24
|
+
accessToken = newTokens.accessToken;
|
|
31
25
|
response = await this.performRequest(endpoint, accessToken, options);
|
|
32
26
|
}
|
|
33
27
|
if (response.status === 403) {
|
|
@@ -81,14 +75,21 @@ class GraphClient {
|
|
|
81
75
|
} else {
|
|
82
76
|
logger.info("GraphClient: Refreshing token with public client");
|
|
83
77
|
}
|
|
84
|
-
const response = await refreshAccessToken(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
const response = await refreshAccessToken(
|
|
79
|
+
refreshToken,
|
|
80
|
+
clientId,
|
|
81
|
+
clientSecret,
|
|
82
|
+
tenantId,
|
|
83
|
+
this.secrets.cloudType
|
|
84
|
+
);
|
|
85
|
+
return {
|
|
86
|
+
accessToken: response.access_token,
|
|
87
|
+
refreshToken: response.refresh_token
|
|
88
|
+
};
|
|
89
89
|
}
|
|
90
90
|
async performRequest(endpoint, accessToken, options) {
|
|
91
|
-
const
|
|
91
|
+
const cloudEndpoints = getCloudEndpoints(this.secrets.cloudType);
|
|
92
|
+
const url = `${cloudEndpoints.graphApi}/v1.0${endpoint}`;
|
|
92
93
|
const headers = {
|
|
93
94
|
Authorization: `Bearer ${accessToken}`,
|
|
94
95
|
"Content-Type": "application/json",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logger from "../logger.js";
|
|
2
|
+
import { getCloudEndpoints } from "../cloud-config.js";
|
|
2
3
|
const microsoftBearerTokenAuthMiddleware = (req, res, next) => {
|
|
3
4
|
const authHeader = req.headers.authorization;
|
|
4
5
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
@@ -13,7 +14,8 @@ const microsoftBearerTokenAuthMiddleware = (req, res, next) => {
|
|
|
13
14
|
};
|
|
14
15
|
next();
|
|
15
16
|
};
|
|
16
|
-
async function exchangeCodeForToken(code, redirectUri, clientId, clientSecret, tenantId = "common", codeVerifier) {
|
|
17
|
+
async function exchangeCodeForToken(code, redirectUri, clientId, clientSecret, tenantId = "common", codeVerifier, cloudType = "global") {
|
|
18
|
+
const cloudEndpoints = getCloudEndpoints(cloudType);
|
|
17
19
|
const params = new URLSearchParams({
|
|
18
20
|
grant_type: "authorization_code",
|
|
19
21
|
code,
|
|
@@ -26,7 +28,7 @@ async function exchangeCodeForToken(code, redirectUri, clientId, clientSecret, t
|
|
|
26
28
|
if (codeVerifier) {
|
|
27
29
|
params.append("code_verifier", codeVerifier);
|
|
28
30
|
}
|
|
29
|
-
const response = await fetch(
|
|
31
|
+
const response = await fetch(`${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/token`, {
|
|
30
32
|
method: "POST",
|
|
31
33
|
headers: {
|
|
32
34
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
@@ -40,7 +42,8 @@ async function exchangeCodeForToken(code, redirectUri, clientId, clientSecret, t
|
|
|
40
42
|
}
|
|
41
43
|
return response.json();
|
|
42
44
|
}
|
|
43
|
-
async function refreshAccessToken(refreshToken, clientId, clientSecret, tenantId = "common") {
|
|
45
|
+
async function refreshAccessToken(refreshToken, clientId, clientSecret, tenantId = "common", cloudType = "global") {
|
|
46
|
+
const cloudEndpoints = getCloudEndpoints(cloudType);
|
|
44
47
|
const params = new URLSearchParams({
|
|
45
48
|
grant_type: "refresh_token",
|
|
46
49
|
refresh_token: refreshToken,
|
|
@@ -49,7 +52,7 @@ async function refreshAccessToken(refreshToken, clientId, clientSecret, tenantId
|
|
|
49
52
|
if (clientSecret) {
|
|
50
53
|
params.append("client_secret", clientSecret);
|
|
51
54
|
}
|
|
52
|
-
const response = await fetch(
|
|
55
|
+
const response = await fetch(`${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/token`, {
|
|
53
56
|
method: "POST",
|
|
54
57
|
headers: {
|
|
55
58
|
"Content-Type": "application/x-www-form-urlencoded"
|
package/dist/oauth-provider.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { ProxyOAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js";
|
|
2
2
|
import logger from "./logger.js";
|
|
3
|
+
import { getCloudEndpoints } from "./cloud-config.js";
|
|
3
4
|
class MicrosoftOAuthProvider extends ProxyOAuthServerProvider {
|
|
4
5
|
constructor(authManager, secrets) {
|
|
5
6
|
const tenantId = secrets.tenantId || "common";
|
|
6
7
|
const clientId = secrets.clientId;
|
|
8
|
+
const cloudEndpoints = getCloudEndpoints(secrets.cloudType);
|
|
7
9
|
super({
|
|
8
10
|
endpoints: {
|
|
9
|
-
authorizationUrl:
|
|
10
|
-
tokenUrl:
|
|
11
|
-
revocationUrl:
|
|
11
|
+
authorizationUrl: `${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/authorize`,
|
|
12
|
+
tokenUrl: `${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/token`,
|
|
13
|
+
revocationUrl: `${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/logout`
|
|
12
14
|
},
|
|
13
15
|
verifyAccessToken: async (token) => {
|
|
14
16
|
try {
|
|
15
|
-
const response = await fetch(
|
|
17
|
+
const response = await fetch(`${cloudEndpoints.graphApi}/v1.0/me`, {
|
|
16
18
|
headers: {
|
|
17
19
|
Authorization: `Bearer ${token}`
|
|
18
20
|
}
|
package/dist/secrets.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import logger from "./logger.js";
|
|
2
|
+
import { parseCloudType } from "./cloud-config.js";
|
|
2
3
|
class EnvironmentSecretsProvider {
|
|
3
4
|
async getSecrets() {
|
|
4
5
|
return {
|
|
5
6
|
clientId: process.env.MS365_MCP_CLIENT_ID || "",
|
|
6
7
|
tenantId: process.env.MS365_MCP_TENANT_ID || "common",
|
|
7
|
-
clientSecret: process.env.MS365_MCP_CLIENT_SECRET
|
|
8
|
+
clientSecret: process.env.MS365_MCP_CLIENT_SECRET,
|
|
9
|
+
cloudType: parseCloudType(process.env.MS365_MCP_CLOUD_TYPE)
|
|
8
10
|
};
|
|
9
11
|
}
|
|
10
12
|
}
|
|
@@ -18,11 +20,14 @@ class KeyVaultSecretsProvider {
|
|
|
18
20
|
const credential = new DefaultAzureCredential();
|
|
19
21
|
const client = new SecretClient(this.vaultUrl, credential);
|
|
20
22
|
logger.info(`Fetching secrets from Key Vault: ${this.vaultUrl}`);
|
|
21
|
-
const [clientIdSecret, tenantIdSecret, clientSecretResult] = await Promise.all(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const [clientIdSecret, tenantIdSecret, clientSecretResult, cloudTypeResult] = await Promise.all(
|
|
24
|
+
[
|
|
25
|
+
client.getSecret("ms365-mcp-client-id"),
|
|
26
|
+
client.getSecret("ms365-mcp-tenant-id").catch(() => null),
|
|
27
|
+
client.getSecret("ms365-mcp-client-secret").catch(() => null),
|
|
28
|
+
client.getSecret("ms365-mcp-cloud-type").catch(() => null)
|
|
29
|
+
]
|
|
30
|
+
);
|
|
26
31
|
if (!clientIdSecret.value) {
|
|
27
32
|
throw new Error("Required secret ms365-mcp-client-id not found in Key Vault");
|
|
28
33
|
}
|
|
@@ -30,7 +35,8 @@ class KeyVaultSecretsProvider {
|
|
|
30
35
|
return {
|
|
31
36
|
clientId: clientIdSecret.value,
|
|
32
37
|
tenantId: tenantIdSecret?.value || "common",
|
|
33
|
-
clientSecret: clientSecretResult?.value
|
|
38
|
+
clientSecret: clientSecretResult?.value,
|
|
39
|
+
cloudType: parseCloudType(cloudTypeResult?.value)
|
|
34
40
|
};
|
|
35
41
|
}
|
|
36
42
|
}
|
package/dist/server.js
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
refreshAccessToken
|
|
16
16
|
} from "./lib/microsoft-auth.js";
|
|
17
17
|
import { getSecrets } from "./secrets.js";
|
|
18
|
+
import { getCloudEndpoints } from "./cloud-config.js";
|
|
19
|
+
import { requestContext } from "./request-context.js";
|
|
18
20
|
function parseHttpOption(httpOption) {
|
|
19
21
|
if (typeof httpOption === "boolean") {
|
|
20
22
|
return { host: void 0, port: 3e3 };
|
|
@@ -134,8 +136,9 @@ class MicrosoftGraphServer {
|
|
|
134
136
|
const url = new URL(req.url, `${req.protocol}://${req.get("host")}`);
|
|
135
137
|
const tenantId = this.secrets?.tenantId || "common";
|
|
136
138
|
const clientId = this.secrets.clientId;
|
|
139
|
+
const cloudEndpoints = getCloudEndpoints(this.secrets.cloudType);
|
|
137
140
|
const microsoftAuthUrl = new URL(
|
|
138
|
-
|
|
141
|
+
`${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/authorize`
|
|
139
142
|
);
|
|
140
143
|
const allowedParams = [
|
|
141
144
|
"response_type",
|
|
@@ -201,7 +204,8 @@ class MicrosoftGraphServer {
|
|
|
201
204
|
clientId,
|
|
202
205
|
clientSecret,
|
|
203
206
|
tenantId,
|
|
204
|
-
body.code_verifier
|
|
207
|
+
body.code_verifier,
|
|
208
|
+
this.secrets.cloudType
|
|
205
209
|
);
|
|
206
210
|
res.json(result);
|
|
207
211
|
} else if (body.grant_type === "refresh_token") {
|
|
@@ -217,7 +221,8 @@ class MicrosoftGraphServer {
|
|
|
217
221
|
body.refresh_token,
|
|
218
222
|
clientId,
|
|
219
223
|
clientSecret,
|
|
220
|
-
tenantId
|
|
224
|
+
tenantId,
|
|
225
|
+
this.secrets.cloudType
|
|
221
226
|
);
|
|
222
227
|
res.json(result);
|
|
223
228
|
} else {
|
|
@@ -244,13 +249,7 @@ class MicrosoftGraphServer {
|
|
|
244
249
|
"/mcp",
|
|
245
250
|
microsoftBearerTokenAuthMiddleware,
|
|
246
251
|
async (req, res) => {
|
|
247
|
-
|
|
248
|
-
if (req.microsoftAuth) {
|
|
249
|
-
this.graphClient.setOAuthTokens(
|
|
250
|
-
req.microsoftAuth.accessToken,
|
|
251
|
-
req.microsoftAuth.refreshToken
|
|
252
|
-
);
|
|
253
|
-
}
|
|
252
|
+
const handler = async () => {
|
|
254
253
|
const transport = new StreamableHTTPServerTransport({
|
|
255
254
|
sessionIdGenerator: void 0
|
|
256
255
|
// Stateless mode
|
|
@@ -260,6 +259,19 @@ class MicrosoftGraphServer {
|
|
|
260
259
|
});
|
|
261
260
|
await this.server.connect(transport);
|
|
262
261
|
await transport.handleRequest(req, res, void 0);
|
|
262
|
+
};
|
|
263
|
+
try {
|
|
264
|
+
if (req.microsoftAuth) {
|
|
265
|
+
await requestContext.run(
|
|
266
|
+
{
|
|
267
|
+
accessToken: req.microsoftAuth.accessToken,
|
|
268
|
+
refreshToken: req.microsoftAuth.refreshToken
|
|
269
|
+
},
|
|
270
|
+
handler
|
|
271
|
+
);
|
|
272
|
+
} else {
|
|
273
|
+
await handler();
|
|
274
|
+
}
|
|
263
275
|
} catch (error) {
|
|
264
276
|
logger.error("Error handling MCP GET request:", error);
|
|
265
277
|
if (!res.headersSent) {
|
|
@@ -279,13 +291,7 @@ class MicrosoftGraphServer {
|
|
|
279
291
|
"/mcp",
|
|
280
292
|
microsoftBearerTokenAuthMiddleware,
|
|
281
293
|
async (req, res) => {
|
|
282
|
-
|
|
283
|
-
if (req.microsoftAuth) {
|
|
284
|
-
this.graphClient.setOAuthTokens(
|
|
285
|
-
req.microsoftAuth.accessToken,
|
|
286
|
-
req.microsoftAuth.refreshToken
|
|
287
|
-
);
|
|
288
|
-
}
|
|
294
|
+
const handler = async () => {
|
|
289
295
|
const transport = new StreamableHTTPServerTransport({
|
|
290
296
|
sessionIdGenerator: void 0
|
|
291
297
|
// Stateless mode
|
|
@@ -295,6 +301,19 @@ class MicrosoftGraphServer {
|
|
|
295
301
|
});
|
|
296
302
|
await this.server.connect(transport);
|
|
297
303
|
await transport.handleRequest(req, res, req.body);
|
|
304
|
+
};
|
|
305
|
+
try {
|
|
306
|
+
if (req.microsoftAuth) {
|
|
307
|
+
await requestContext.run(
|
|
308
|
+
{
|
|
309
|
+
accessToken: req.microsoftAuth.accessToken,
|
|
310
|
+
refreshToken: req.microsoftAuth.refreshToken
|
|
311
|
+
},
|
|
312
|
+
handler
|
|
313
|
+
);
|
|
314
|
+
} else {
|
|
315
|
+
await handler();
|
|
316
|
+
}
|
|
298
317
|
} catch (error) {
|
|
299
318
|
logger.error("Error handling MCP POST request:", error);
|
|
300
319
|
if (!res.headersSent) {
|
package/logs/mcp-server.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
2026-01-19 11:31:37 INFO: Using environment variables for secrets
|
|
2
|
+
2026-01-19 11:31:37 INFO: Using environment variables for secrets
|
|
3
|
+
2026-01-19 11:31:37 INFO: Using environment variables for secrets
|
|
4
|
+
2026-01-19 11:31:37 INFO: Using environment variables for secrets
|
|
5
|
+
2026-01-19 11:31:37 INFO: Using environment variables for secrets
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.1",
|
|
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",
|