@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 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
+ ![Open WebUI MCP Connection](https://github.com/user-attachments/assets/dcab71dd-cf02-4bcb-b7db-5725d6be4064)
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();
@@ -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
- let accessToken = options.accessToken || this.accessToken || await this.authManager.getToken();
20
- let refreshToken = options.refreshToken || this.refreshToken;
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 = this.accessToken || 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
- this.accessToken = response.access_token;
93
- if (response.refresh_token) {
94
- this.refreshToken = response.refresh_token;
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);
@@ -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
- queryParams[fixedParamName] = `${paramValue}`;
48
+ if (paramValue !== "" && paramValue != null) {
49
+ queryParams[fixedParamName] = `${paramValue}`;
50
+ }
49
51
  break;
50
52
  case "Body":
51
53
  if (paramDef.schema) {
@@ -0,0 +1,9 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const requestContext = new AsyncLocalStorage();
3
+ function getRequestTokens() {
4
+ return requestContext.getStore();
5
+ }
6
+ export {
7
+ getRequestTokens,
8
+ requestContext
9
+ };
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: parseCloudType(process.env.MS365_MCP_CLOUD_TYPE)
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
- res.json({
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
- if (clientSecret) {
196
- logger.info("Token endpoint: Using confidential client with client_secret");
197
- } else {
198
- logger.info("Token endpoint: Using public client without client_secret");
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
- try {
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
- try {
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
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": ["eirikb"]
4
+ }
@@ -1,5 +1,5 @@
1
- 2026-01-12 13:28:46 INFO: Using environment variables for secrets
2
- 2026-01-12 13:28:46 INFO: Using environment variables for secrets
3
- 2026-01-12 13:28:46 INFO: Using environment variables for secrets
4
- 2026-01-12 13:28:46 INFO: Using environment variables for secrets
5
- 2026-01-12 13:28:46 INFO: Using environment variables for secrets
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.30.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": "^12.0.2",
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": "^24.2.7",
66
+ "semantic-release": "^25.0.2",
67
67
  "tsup": "^8.5.0",
68
68
  "tsx": "^4.19.4",
69
69
  "typescript": "^5.8.3",