@softeria/ms-365-mcp-server 0.30.0 → 0.31.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 +35 -0
- package/dist/cli.js +4 -1
- package/dist/graph-client.js +10 -17
- package/dist/graph-tools.js +3 -1
- package/dist/request-context.js +9 -0
- package/dist/secrets.js +4 -3
- package/dist/server.js +59 -21
- package/glama.json +4 -0
- package/logs/mcp-server.log +5 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -269,6 +269,40 @@ claude mcp add ms365-china -s user -- cmd /c "npx -y @softeria/ms-365-mcp-server
|
|
|
269
269
|
For other interfaces that support MCPs, please refer to their respective documentation for the correct
|
|
270
270
|
integration method.
|
|
271
271
|
|
|
272
|
+
### Open WebUI
|
|
273
|
+
|
|
274
|
+
Open WebUI supports MCP servers via HTTP transport with OAuth 2.1.
|
|
275
|
+
|
|
276
|
+
1. Start the server with HTTP mode and dynamic registration enabled:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
npx @softeria/ms-365-mcp-server --http --enable-dynamic-registration
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
2. In Open WebUI, go to **Admin Settings → Tools** (`/admin/settings/tools`) → **Add Connection**:
|
|
283
|
+
- **Type**: MCP Streamable HTTP
|
|
284
|
+
- **URL**: Your MCP server URL with `/mcp` path
|
|
285
|
+
- **Auth**: OAuth 2.1
|
|
286
|
+
|
|
287
|
+
3. Click **Register Client**.
|
|
288
|
+
|
|
289
|
+
> **Note**: The `--enable-dynamic-registration` is required for Open WebUI to work. If using a custom Azure Entra app, add your redirect URI under "Mobile and desktop applications" platform (not "Single-page application").
|
|
290
|
+
|
|
291
|
+
**Quick test setup** using the default Azure app (ID `ms-365` and `localhost:8080` are pre-configured):
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
docker run -d -p 8080:8080 \
|
|
295
|
+
-e WEBUI_AUTH=false \
|
|
296
|
+
-e OPENAI_API_KEY \
|
|
297
|
+
ghcr.io/open-webui/open-webui:main
|
|
298
|
+
|
|
299
|
+
npx @softeria/ms-365-mcp-server --http --enable-dynamic-registration
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Then add connection with URL `http://localhost:3000/mcp` and ID `ms-365`.
|
|
303
|
+
|
|
304
|
+

|
|
305
|
+
|
|
272
306
|
### Local Development
|
|
273
307
|
|
|
274
308
|
For local development or testing:
|
|
@@ -434,6 +468,7 @@ When running as an MCP server, the following options can be used:
|
|
|
434
468
|
--http [port] Use Streamable HTTP transport instead of stdio (optionally specify port, default: 3000)
|
|
435
469
|
Starts Express.js server with MCP endpoint at /mcp
|
|
436
470
|
--enable-auth-tools Enable login/logout tools when using HTTP mode (disabled by default in HTTP mode)
|
|
471
|
+
--enable-dynamic-registration Enable OAuth Dynamic Client Registration endpoint (required for Open WebUI)
|
|
437
472
|
--enabled-tools <pattern> Filter tools using regex pattern (e.g., "excel|contact" to enable Excel and Contact tools)
|
|
438
473
|
--preset <names> Use preset tool categories (comma-separated). See "Tool Presets" section above
|
|
439
474
|
--list-presets List all available presets and exit
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,10 @@ 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)").option("--cloud <type>", "Microsoft cloud environment: global (default) or china (21Vianet)")
|
|
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)").option(
|
|
27
|
+
"--enable-dynamic-registration",
|
|
28
|
+
"Enable OAuth Dynamic Client Registration endpoint (required for some MCP clients like Open WebUI)"
|
|
29
|
+
);
|
|
27
30
|
function parseArgs() {
|
|
28
31
|
program.parse();
|
|
29
32
|
const options = program.opts();
|
package/dist/graph-client.js
CHANGED
|
@@ -2,33 +2,26 @@ 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
4
|
import { getCloudEndpoints } from "./cloud-config.js";
|
|
5
|
+
import { getRequestTokens } from "./request-context.js";
|
|
5
6
|
class GraphClient {
|
|
6
7
|
constructor(authManager, secrets, outputFormat = "json") {
|
|
7
|
-
this.accessToken = null;
|
|
8
|
-
this.refreshToken = null;
|
|
9
8
|
this.outputFormat = "json";
|
|
10
9
|
this.authManager = authManager;
|
|
11
10
|
this.secrets = secrets;
|
|
12
11
|
this.outputFormat = outputFormat;
|
|
13
12
|
}
|
|
14
|
-
setOAuthTokens(accessToken, refreshToken) {
|
|
15
|
-
this.accessToken = accessToken;
|
|
16
|
-
this.refreshToken = refreshToken || null;
|
|
17
|
-
}
|
|
18
13
|
async makeRequest(endpoint, options = {}) {
|
|
19
|
-
|
|
20
|
-
let
|
|
14
|
+
const contextTokens = getRequestTokens();
|
|
15
|
+
let accessToken = options.accessToken ?? contextTokens?.accessToken ?? await this.authManager.getToken();
|
|
16
|
+
const refreshToken = options.refreshToken ?? contextTokens?.refreshToken;
|
|
21
17
|
if (!accessToken) {
|
|
22
18
|
throw new Error("No access token available");
|
|
23
19
|
}
|
|
24
20
|
try {
|
|
25
21
|
let response = await this.performRequest(endpoint, accessToken, options);
|
|
26
22
|
if (response.status === 401 && refreshToken) {
|
|
27
|
-
await this.refreshAccessToken(refreshToken);
|
|
28
|
-
accessToken =
|
|
29
|
-
if (!accessToken) {
|
|
30
|
-
throw new Error("Failed to refresh access token");
|
|
31
|
-
}
|
|
23
|
+
const newTokens = await this.refreshAccessToken(refreshToken);
|
|
24
|
+
accessToken = newTokens.accessToken;
|
|
32
25
|
response = await this.performRequest(endpoint, accessToken, options);
|
|
33
26
|
}
|
|
34
27
|
if (response.status === 403) {
|
|
@@ -89,10 +82,10 @@ class GraphClient {
|
|
|
89
82
|
tenantId,
|
|
90
83
|
this.secrets.cloudType
|
|
91
84
|
);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
85
|
+
return {
|
|
86
|
+
accessToken: response.access_token,
|
|
87
|
+
refreshToken: response.refresh_token
|
|
88
|
+
};
|
|
96
89
|
}
|
|
97
90
|
async performRequest(endpoint, accessToken, options) {
|
|
98
91
|
const cloudEndpoints = getCloudEndpoints(this.secrets.cloudType);
|
package/dist/graph-tools.js
CHANGED
|
@@ -45,7 +45,9 @@ async function executeGraphTool(tool, config, graphClient, params) {
|
|
|
45
45
|
path2 = path2.replace(`{${paramName}}`, encodeURIComponent(paramValue)).replace(`:${paramName}`, encodeURIComponent(paramValue));
|
|
46
46
|
break;
|
|
47
47
|
case "Query":
|
|
48
|
-
|
|
48
|
+
if (paramValue !== "" && paramValue != null) {
|
|
49
|
+
queryParams[fixedParamName] = `${paramValue}`;
|
|
50
|
+
}
|
|
49
51
|
break;
|
|
50
52
|
case "Body":
|
|
51
53
|
if (paramDef.schema) {
|
package/dist/secrets.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import logger from "./logger.js";
|
|
2
|
-
import { parseCloudType } from "./cloud-config.js";
|
|
2
|
+
import { parseCloudType, getDefaultClientId } from "./cloud-config.js";
|
|
3
3
|
class EnvironmentSecretsProvider {
|
|
4
4
|
async getSecrets() {
|
|
5
|
+
const cloudType = parseCloudType(process.env.MS365_MCP_CLOUD_TYPE);
|
|
5
6
|
return {
|
|
6
|
-
clientId: process.env.MS365_MCP_CLIENT_ID ||
|
|
7
|
+
clientId: process.env.MS365_MCP_CLIENT_ID || getDefaultClientId(cloudType),
|
|
7
8
|
tenantId: process.env.MS365_MCP_TENANT_ID || "common",
|
|
8
9
|
clientSecret: process.env.MS365_MCP_CLIENT_SECRET,
|
|
9
|
-
cloudType
|
|
10
|
+
cloudType
|
|
10
11
|
};
|
|
11
12
|
}
|
|
12
13
|
}
|
package/dist/server.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "./lib/microsoft-auth.js";
|
|
17
17
|
import { getSecrets } from "./secrets.js";
|
|
18
18
|
import { getCloudEndpoints } from "./cloud-config.js";
|
|
19
|
+
import { requestContext } from "./request-context.js";
|
|
19
20
|
function parseHttpOption(httpOption) {
|
|
20
21
|
if (typeof httpOption === "boolean") {
|
|
21
22
|
return { host: void 0, port: 3e3 };
|
|
@@ -107,7 +108,7 @@ class MicrosoftGraphServer {
|
|
|
107
108
|
const protocol = req.secure ? "https" : "http";
|
|
108
109
|
const url = new URL(`${protocol}://${req.get("host")}`);
|
|
109
110
|
const scopes = buildScopesFromEndpoints(this.options.orgMode, this.options.enabledTools);
|
|
110
|
-
|
|
111
|
+
const metadata = {
|
|
111
112
|
issuer: url.origin,
|
|
112
113
|
authorization_endpoint: `${url.origin}/authorize`,
|
|
113
114
|
token_endpoint: `${url.origin}/token`,
|
|
@@ -117,7 +118,11 @@ class MicrosoftGraphServer {
|
|
|
117
118
|
token_endpoint_auth_methods_supported: ["none"],
|
|
118
119
|
code_challenge_methods_supported: ["S256"],
|
|
119
120
|
scopes_supported: scopes
|
|
120
|
-
}
|
|
121
|
+
};
|
|
122
|
+
if (this.options.enableDynamicRegistration) {
|
|
123
|
+
metadata.registration_endpoint = `${url.origin}/register`;
|
|
124
|
+
}
|
|
125
|
+
res.json(metadata);
|
|
121
126
|
});
|
|
122
127
|
app.get("/.well-known/oauth-protected-resource", async (req, res) => {
|
|
123
128
|
const protocol = req.secure ? "https" : "http";
|
|
@@ -131,6 +136,22 @@ class MicrosoftGraphServer {
|
|
|
131
136
|
resource_documentation: `${url.origin}`
|
|
132
137
|
});
|
|
133
138
|
});
|
|
139
|
+
if (this.options.enableDynamicRegistration) {
|
|
140
|
+
app.post("/register", async (req, res) => {
|
|
141
|
+
const body = req.body;
|
|
142
|
+
logger.info("Client registration request", { body });
|
|
143
|
+
const clientId = `mcp-client-${Date.now()}`;
|
|
144
|
+
res.status(201).json({
|
|
145
|
+
client_id: clientId,
|
|
146
|
+
client_id_issued_at: Math.floor(Date.now() / 1e3),
|
|
147
|
+
redirect_uris: body.redirect_uris || [],
|
|
148
|
+
grant_types: body.grant_types || ["authorization_code", "refresh_token"],
|
|
149
|
+
response_types: body.response_types || ["code"],
|
|
150
|
+
token_endpoint_auth_method: body.token_endpoint_auth_method || "none",
|
|
151
|
+
client_name: body.client_name || "MCP Client"
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
134
155
|
app.get("/authorize", async (req, res) => {
|
|
135
156
|
const url = new URL(req.url, `${req.protocol}://${req.get("host")}`);
|
|
136
157
|
const tenantId = this.secrets?.tenantId || "common";
|
|
@@ -192,11 +213,14 @@ class MicrosoftGraphServer {
|
|
|
192
213
|
const tenantId = this.secrets?.tenantId || "common";
|
|
193
214
|
const clientId = this.secrets.clientId;
|
|
194
215
|
const clientSecret = this.secrets?.clientSecret;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
216
|
+
logger.info("Token endpoint: authorization_code exchange", {
|
|
217
|
+
redirect_uri: body.redirect_uri,
|
|
218
|
+
has_code: !!body.code,
|
|
219
|
+
has_code_verifier: !!body.code_verifier,
|
|
220
|
+
clientId,
|
|
221
|
+
tenantId,
|
|
222
|
+
hasClientSecret: !!clientSecret
|
|
223
|
+
});
|
|
200
224
|
const result = await exchangeCodeForToken(
|
|
201
225
|
body.code,
|
|
202
226
|
body.redirect_uri,
|
|
@@ -248,13 +272,7 @@ class MicrosoftGraphServer {
|
|
|
248
272
|
"/mcp",
|
|
249
273
|
microsoftBearerTokenAuthMiddleware,
|
|
250
274
|
async (req, res) => {
|
|
251
|
-
|
|
252
|
-
if (req.microsoftAuth) {
|
|
253
|
-
this.graphClient.setOAuthTokens(
|
|
254
|
-
req.microsoftAuth.accessToken,
|
|
255
|
-
req.microsoftAuth.refreshToken
|
|
256
|
-
);
|
|
257
|
-
}
|
|
275
|
+
const handler = async () => {
|
|
258
276
|
const transport = new StreamableHTTPServerTransport({
|
|
259
277
|
sessionIdGenerator: void 0
|
|
260
278
|
// Stateless mode
|
|
@@ -264,6 +282,19 @@ class MicrosoftGraphServer {
|
|
|
264
282
|
});
|
|
265
283
|
await this.server.connect(transport);
|
|
266
284
|
await transport.handleRequest(req, res, void 0);
|
|
285
|
+
};
|
|
286
|
+
try {
|
|
287
|
+
if (req.microsoftAuth) {
|
|
288
|
+
await requestContext.run(
|
|
289
|
+
{
|
|
290
|
+
accessToken: req.microsoftAuth.accessToken,
|
|
291
|
+
refreshToken: req.microsoftAuth.refreshToken
|
|
292
|
+
},
|
|
293
|
+
handler
|
|
294
|
+
);
|
|
295
|
+
} else {
|
|
296
|
+
await handler();
|
|
297
|
+
}
|
|
267
298
|
} catch (error) {
|
|
268
299
|
logger.error("Error handling MCP GET request:", error);
|
|
269
300
|
if (!res.headersSent) {
|
|
@@ -283,13 +314,7 @@ class MicrosoftGraphServer {
|
|
|
283
314
|
"/mcp",
|
|
284
315
|
microsoftBearerTokenAuthMiddleware,
|
|
285
316
|
async (req, res) => {
|
|
286
|
-
|
|
287
|
-
if (req.microsoftAuth) {
|
|
288
|
-
this.graphClient.setOAuthTokens(
|
|
289
|
-
req.microsoftAuth.accessToken,
|
|
290
|
-
req.microsoftAuth.refreshToken
|
|
291
|
-
);
|
|
292
|
-
}
|
|
317
|
+
const handler = async () => {
|
|
293
318
|
const transport = new StreamableHTTPServerTransport({
|
|
294
319
|
sessionIdGenerator: void 0
|
|
295
320
|
// Stateless mode
|
|
@@ -299,6 +324,19 @@ class MicrosoftGraphServer {
|
|
|
299
324
|
});
|
|
300
325
|
await this.server.connect(transport);
|
|
301
326
|
await transport.handleRequest(req, res, req.body);
|
|
327
|
+
};
|
|
328
|
+
try {
|
|
329
|
+
if (req.microsoftAuth) {
|
|
330
|
+
await requestContext.run(
|
|
331
|
+
{
|
|
332
|
+
accessToken: req.microsoftAuth.accessToken,
|
|
333
|
+
refreshToken: req.microsoftAuth.refreshToken
|
|
334
|
+
},
|
|
335
|
+
handler
|
|
336
|
+
);
|
|
337
|
+
} else {
|
|
338
|
+
await handler();
|
|
339
|
+
}
|
|
302
340
|
} catch (error) {
|
|
303
341
|
logger.error("Error handling MCP POST request:", error);
|
|
304
342
|
if (!res.headersSent) {
|
package/glama.json
ADDED
package/logs/mcp-server.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
2026-01-
|
|
2
|
-
2026-01-
|
|
3
|
-
2026-01-
|
|
4
|
-
2026-01-
|
|
5
|
-
2026-01-
|
|
1
|
+
2026-01-27 11:31:49 INFO: Using environment variables for secrets
|
|
2
|
+
2026-01-27 11:31:49 INFO: Using environment variables for secrets
|
|
3
|
+
2026-01-27 11:31:49 INFO: Using environment variables for secrets
|
|
4
|
+
2026-01-27 11:31:49 INFO: Using environment variables for secrets
|
|
5
|
+
2026-01-27 11:31:49 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.31.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",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@semantic-release/exec": "^7.1.0",
|
|
54
54
|
"@semantic-release/git": "^10.0.1",
|
|
55
55
|
"@semantic-release/github": "^11.0.3",
|
|
56
|
-
"@semantic-release/npm": "^
|
|
56
|
+
"@semantic-release/npm": "^13.1.3",
|
|
57
57
|
"@types/express": "^5.0.3",
|
|
58
58
|
"@types/node": "^22.15.15",
|
|
59
59
|
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"globals": "^16.3.0",
|
|
64
64
|
"patch-package": "^8.0.1",
|
|
65
65
|
"prettier": "^3.5.3",
|
|
66
|
-
"semantic-release": "^
|
|
66
|
+
"semantic-release": "^25.0.2",
|
|
67
67
|
"tsup": "^8.5.0",
|
|
68
68
|
"tsx": "^4.19.4",
|
|
69
69
|
"typescript": "^5.8.3",
|