@softeria/ms-365-mcp-server 0.24.0 → 0.24.2

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/Dockerfile ADDED
@@ -0,0 +1,13 @@
1
+ FROM node:20-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package*.json ./
6
+ RUN npm ci
7
+
8
+ COPY . .
9
+ RUN npm run build
10
+ RUN npm prune --production
11
+
12
+ ENTRYPOINT ["node", "dist/index.js"]
13
+ CMD ["--http"]
package/README.md CHANGED
@@ -287,19 +287,30 @@ registration:
287
287
  - Navigate to Azure Active Directory → App registrations → New registration
288
288
  - Set name: "MS365 MCP Server"
289
289
 
290
- 1. **Configure Redirect URIs**:
291
- Add these redirect URIs for testing with MCP Inspector (`npm run inspector`):
290
+ 2. **Configure Redirect URIs**:
292
291
 
293
- - `http://localhost:6274/oauth/callback`
294
- - `http://localhost:6274/oauth/callback/debug`
295
- - `http://localhost:3000/callback` (optional, for server callback)
292
+ - **Configure the OAuth callback URI**: Go to your app registration and on the left side, go to Authentication.
293
+ - Under Platform configurations:
294
+ - Click Add a platform (if you don’t already see one for "Mobile and desktop applications" / "Public client").
295
+ - Choose Mobile and desktop applications or Public client/native (mobile & desktop) (label depends on portal version).
296
296
 
297
- 1. **Get Credentials**:
297
+ 3. **Testing with MCP Inspector (`npm run inspector`)**:
298
+
299
+ - Go to your app registration and on the left side, go to Authentication.
300
+ - Under Platform configurations:
301
+ - Click Add a platform (if you don’t already see one for "Web").
302
+ - Choose Web.
303
+ - Configure the following redirect URIs
304
+ - `http://localhost:6274/oauth/callback`
305
+ - `http://localhost:6274/oauth/callback/debug`
306
+ - `http://localhost:3000/callback` (optional, for server callback)
307
+
308
+ 4. **Get Credentials**:
298
309
 
299
310
  - Copy the **Application (client) ID** from Overview page
300
311
  - Go to Certificates & secrets → New client secret → Copy the secret value
301
312
 
302
- 1. **Configure Environment Variables**:
313
+ 5. **Configure Environment Variables**:
303
314
  Create a `.env` file in your project root:
304
315
  ```env
305
316
  MS365_MCP_CLIENT_ID=your-azure-ad-app-client-id-here
package/dist/auth.js CHANGED
@@ -1,9 +1,25 @@
1
1
  import { PublicClientApplication } from "@azure/msal-node";
2
- import keytar from "keytar";
3
2
  import logger from "./logger.js";
4
3
  import fs, { existsSync, readFileSync } from "fs";
5
4
  import { fileURLToPath } from "url";
6
5
  import path from "path";
6
+ let keytar = null;
7
+ async function getKeytar() {
8
+ if (keytar === void 0) {
9
+ return null;
10
+ }
11
+ if (keytar === null) {
12
+ try {
13
+ keytar = await import("keytar");
14
+ return keytar;
15
+ } catch (error) {
16
+ logger.info("keytar not available, using file-based credential storage");
17
+ keytar = void 0;
18
+ return null;
19
+ }
20
+ }
21
+ return keytar;
22
+ }
7
23
  const __filename = fileURLToPath(import.meta.url);
8
24
  const __dirname = path.dirname(__filename);
9
25
  const endpointsData = JSON.parse(
@@ -31,9 +47,23 @@ const SCOPE_HIERARCHY = {
31
47
  "Tasks.ReadWrite": ["Tasks.Read"],
32
48
  "Contacts.ReadWrite": ["Contacts.Read"]
33
49
  };
34
- function buildScopesFromEndpoints(includeWorkAccountScopes = false) {
50
+ function buildScopesFromEndpoints(includeWorkAccountScopes = false, enabledToolsPattern) {
35
51
  const scopesSet = /* @__PURE__ */ new Set();
52
+ let enabledToolsRegex;
53
+ if (enabledToolsPattern) {
54
+ try {
55
+ enabledToolsRegex = new RegExp(enabledToolsPattern, "i");
56
+ logger.info(`Building scopes with tool filter pattern: ${enabledToolsPattern}`);
57
+ } catch (error) {
58
+ logger.error(
59
+ `Invalid tool filter regex pattern: ${enabledToolsPattern}. Building scopes without filter.`
60
+ );
61
+ }
62
+ }
36
63
  endpoints.default.forEach((endpoint) => {
64
+ if (enabledToolsRegex && !enabledToolsRegex.test(endpoint.toolName)) {
65
+ return;
66
+ }
37
67
  if (!includeWorkAccountScopes && !endpoint.scopes && endpoint.workScopes) {
38
68
  return;
39
69
  }
@@ -50,7 +80,11 @@ function buildScopesFromEndpoints(includeWorkAccountScopes = false) {
50
80
  scopesSet.add(higherScope);
51
81
  }
52
82
  });
53
- return Array.from(scopesSet);
83
+ const scopes = Array.from(scopesSet);
84
+ if (enabledToolsPattern) {
85
+ logger.info(`Built ${scopes.length} scopes for filtered tools: ${scopes.join(", ")}`);
86
+ }
87
+ return scopes;
54
88
  }
55
89
  class AuthManager {
56
90
  constructor(config = DEFAULT_CONFIG, scopes = buildScopesFromEndpoints()) {
@@ -69,9 +103,12 @@ class AuthManager {
69
103
  try {
70
104
  let cacheData;
71
105
  try {
72
- const cachedData = await keytar.getPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
73
- if (cachedData) {
74
- cacheData = cachedData;
106
+ const kt = await getKeytar();
107
+ if (kt) {
108
+ const cachedData = await kt.getPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
109
+ if (cachedData) {
110
+ cacheData = cachedData;
111
+ }
75
112
  }
76
113
  } catch (keytarError) {
77
114
  logger.warn(
@@ -93,9 +130,12 @@ class AuthManager {
93
130
  try {
94
131
  let selectedAccountData;
95
132
  try {
96
- const cachedData = await keytar.getPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
97
- if (cachedData) {
98
- selectedAccountData = cachedData;
133
+ const kt = await getKeytar();
134
+ if (kt) {
135
+ const cachedData = await kt.getPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
136
+ if (cachedData) {
137
+ selectedAccountData = cachedData;
138
+ }
99
139
  }
100
140
  } catch (keytarError) {
101
141
  logger.warn(
@@ -118,7 +158,12 @@ class AuthManager {
118
158
  try {
119
159
  const cacheData = this.msalApp.getTokenCache().serialize();
120
160
  try {
121
- await keytar.setPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT, cacheData);
161
+ const kt = await getKeytar();
162
+ if (kt) {
163
+ await kt.setPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT, cacheData);
164
+ } else {
165
+ fs.writeFileSync(FALLBACK_PATH, cacheData);
166
+ }
122
167
  } catch (keytarError) {
123
168
  logger.warn(
124
169
  `Keychain save failed, falling back to file storage: ${keytarError.message}`
@@ -133,7 +178,12 @@ class AuthManager {
133
178
  try {
134
179
  const selectedAccountData = JSON.stringify({ accountId: this.selectedAccountId });
135
180
  try {
136
- await keytar.setPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY, selectedAccountData);
181
+ const kt = await getKeytar();
182
+ if (kt) {
183
+ await kt.setPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY, selectedAccountData);
184
+ } else {
185
+ fs.writeFileSync(SELECTED_ACCOUNT_PATH, selectedAccountData);
186
+ }
137
187
  } catch (keytarError) {
138
188
  logger.warn(
139
189
  `Keychain save failed for selected account, falling back to file storage: ${keytarError.message}`
@@ -286,8 +336,11 @@ class AuthManager {
286
336
  this.tokenExpiry = null;
287
337
  this.selectedAccountId = null;
288
338
  try {
289
- await keytar.deletePassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
290
- await keytar.deletePassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
339
+ const kt = await getKeytar();
340
+ if (kt) {
341
+ await kt.deletePassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
342
+ await kt.deletePassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
343
+ }
291
344
  } catch (keytarError) {
292
345
  logger.warn(`Keychain deletion failed: ${keytarError.message}`);
293
346
  }
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ async function main() {
12
12
  if (includeWorkScopes) {
13
13
  logger.info("Organization mode enabled - including work account scopes");
14
14
  }
15
- const scopes = buildScopesFromEndpoints(includeWorkScopes);
15
+ const scopes = buildScopesFromEndpoints(includeWorkScopes, args.enabledTools);
16
16
  const authManager = new AuthManager(void 0, scopes);
17
17
  await authManager.loadTokenCache();
18
18
  if (args.login) {
package/dist/server.js CHANGED
@@ -78,7 +78,7 @@ class MicrosoftGraphServer {
78
78
  app.get("/.well-known/oauth-authorization-server", async (req, res) => {
79
79
  const protocol = req.secure ? "https" : "http";
80
80
  const url = new URL(`${protocol}://${req.get("host")}`);
81
- const scopes = buildScopesFromEndpoints(this.options.orgMode);
81
+ const scopes = buildScopesFromEndpoints(this.options.orgMode, this.options.enabledTools);
82
82
  res.json({
83
83
  issuer: url.origin,
84
84
  authorization_endpoint: `${url.origin}/authorize`,
@@ -95,7 +95,7 @@ class MicrosoftGraphServer {
95
95
  app.get("/.well-known/oauth-protected-resource", async (req, res) => {
96
96
  const protocol = req.secure ? "https" : "http";
97
97
  const url = new URL(`${protocol}://${req.get("host")}`);
98
- const scopes = buildScopesFromEndpoints(this.options.orgMode);
98
+ const scopes = buildScopesFromEndpoints(this.options.orgMode, this.options.enabledTools);
99
99
  res.json({
100
100
  resource: `${url.origin}/mcp`,
101
101
  authorization_servers: [url.origin],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
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",
@@ -40,10 +40,12 @@
40
40
  "dotenv": "^17.0.1",
41
41
  "express": "^5.1.0",
42
42
  "js-yaml": "^4.1.0",
43
- "keytar": "^7.9.0",
44
43
  "winston": "^3.17.0",
45
44
  "zod": "^3.24.2"
46
45
  },
46
+ "optionalDependencies": {
47
+ "keytar": "^7.9.0"
48
+ },
47
49
  "devDependencies": {
48
50
  "@redocly/cli": "^1.34.3",
49
51
  "@semantic-release/exec": "^7.1.0",