@softeria/ms-365-mcp-server 0.10.1 → 0.11.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/README.md CHANGED
@@ -60,6 +60,29 @@ get-sharepoint-site-drive-by-id, list-sharepoint-site-items, get-sharepoint-site
60
60
  get-sharepoint-site-list, list-sharepoint-site-list-items, get-sharepoint-site-list-item,
61
61
  get-sharepoint-sites-delta</sub>
62
62
 
63
+ ### Work Scopes Issues
64
+
65
+ If you're having issues accessing work/school features (Teams, SharePoint, etc.), you should pass the
66
+ `--force-work-scopes` flag!
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "ms365": {
72
+ "command": "npx",
73
+ "args": [
74
+ "-y",
75
+ "@softeria/ms-365-mcp-server",
76
+ "--force-work-scopes"
77
+ ]
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ While the server should attempt to force a re-login when work scopes are needed, passing the flag explicitly is safer
84
+ and ensures proper scope permissions from the start.
85
+
63
86
  **User Profile**
64
87
  <sub>get-current-user</sub>
65
88
 
@@ -146,22 +169,23 @@ MCP clients will automatically handle the OAuth flow when they see the advertise
146
169
 
147
170
  ##### Setting up Azure AD for OAuth Testing
148
171
 
149
- To use OAuth mode with custom Azure credentials (recommended for production), you'll need to set up an Azure AD app registration:
172
+ To use OAuth mode with custom Azure credentials (recommended for production), you'll need to set up an Azure AD app
173
+ registration:
150
174
 
151
175
  1. **Create Azure AD App Registration**:
152
- - Go to [Azure Portal](https://portal.azure.com)
153
- - Navigate to Azure Active Directory → App registrations → New registration
154
- - Set name: "MS365 MCP Server"
176
+ - Go to [Azure Portal](https://portal.azure.com)
177
+ - Navigate to Azure Active Directory → App registrations → New registration
178
+ - Set name: "MS365 MCP Server"
155
179
 
156
180
  2. **Configure Redirect URIs**:
157
181
  Add these redirect URIs for testing with MCP Inspector:
158
- - `http://localhost:6274/oauth/callback`
159
- - `http://localhost:6274/oauth/callback/debug`
160
- - `http://localhost:3000/callback` (optional, for server callback)
182
+ - `http://localhost:6274/oauth/callback`
183
+ - `http://localhost:6274/oauth/callback/debug`
184
+ - `http://localhost:3000/callback` (optional, for server callback)
161
185
 
162
186
  3. **Get Credentials**:
163
- - Copy the **Application (client) ID** from Overview page
164
- - Go to Certificates & secrets → New client secret → Copy the secret value
187
+ - Copy the **Application (client) ID** from Overview page
188
+ - Go to Certificates & secrets → New client secret → Copy the secret value
165
189
 
166
190
  4. **Configure Environment Variables**:
167
191
  Create a `.env` file in your project root:
@@ -175,13 +199,15 @@ With these configured, the server will use your custom Azure app instead of the
175
199
 
176
200
  #### 3. Bring Your Own Token (BYOT)
177
201
 
178
- If you are running ms-365-mcp-server as part of a larger system that manages Microsoft OAuth tokens externally, you can provide an access token directly to this MCP server:
202
+ If you are running ms-365-mcp-server as part of a larger system that manages Microsoft OAuth tokens externally, you can
203
+ provide an access token directly to this MCP server:
179
204
 
180
205
  ```bash
181
206
  MS365_MCP_OAUTH_TOKEN=your_oauth_token npx @softeria/ms-365-mcp-server
182
207
  ```
183
208
 
184
209
  This method:
210
+
185
211
  - Bypasses the interactive authentication flows
186
212
  - Uses your pre-existing OAuth token for Microsoft Graph API requests
187
213
  - Does not handle token refresh (token lifecycle management is your responsibility)
package/dist/auth.js CHANGED
@@ -141,8 +141,9 @@ class AuthManager {
141
141
  };
142
142
  try {
143
143
  logger.info('Requesting device code...');
144
- logger.info(`Scopes are: ${this.scopes.join(', ')}`);
144
+ logger.info(`Requesting scopes: ${this.scopes.join(', ')}`);
145
145
  const response = await this.msalApp.acquireTokenByDeviceCode(deviceCodeRequest);
146
+ logger.info(`Granted scopes: ${response?.scopes?.join(', ') || 'none'}`);
146
147
  logger.info('Device code login successful');
147
148
  this.accessToken = response?.accessToken || null;
148
149
  this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
@@ -47,7 +47,7 @@ class GraphClient {
47
47
  }
48
48
  async makeRequest(endpoint, options = {}) {
49
49
  // Use OAuth tokens if available, otherwise fall back to authManager
50
- let accessToken = options.accessToken || this.accessToken || await this.authManager.getToken();
50
+ let accessToken = options.accessToken || this.accessToken || (await this.authManager.getToken());
51
51
  let refreshToken = options.refreshToken || this.refreshToken;
52
52
  if (!accessToken) {
53
53
  throw new Error('No access token available');
@@ -65,6 +65,24 @@ class GraphClient {
65
65
  // Retry the request with new token
66
66
  return this.performRequest(endpoint, accessToken, options);
67
67
  }
68
+ if (response.status === 403) {
69
+ const errorText = await response.text();
70
+ if (errorText.includes('scope') || errorText.includes('permission')) {
71
+ const hasWorkPermissions = await this.authManager.hasWorkAccountPermissions();
72
+ if (!hasWorkPermissions) {
73
+ logger.info('403 scope error detected, attempting to expand to work account scopes...');
74
+ const expanded = await this.authManager.expandToWorkAccountScopes();
75
+ if (expanded) {
76
+ const newToken = await this.authManager.getToken();
77
+ if (newToken) {
78
+ logger.info('Retrying request with expanded scopes...');
79
+ return this.performRequest(endpoint, newToken, options);
80
+ }
81
+ }
82
+ }
83
+ }
84
+ throw new Error(`Microsoft Graph API scope error: ${response.status} ${response.statusText} - ${errorText}`);
85
+ }
68
86
  if (!response.ok) {
69
87
  throw new Error(`Microsoft Graph API error: ${response.status} ${response.statusText}`);
70
88
  }
@@ -116,7 +134,7 @@ class GraphClient {
116
134
  throw new Error('Excel operation requested without specifying a file');
117
135
  }
118
136
  const headers = {
119
- 'Authorization': `Bearer ${accessToken}`,
137
+ Authorization: `Bearer ${accessToken}`,
120
138
  'Content-Type': 'application/json',
121
139
  ...(sessionId && { 'workbook-session-id': sessionId }),
122
140
  ...options.headers,
package/dist/index.js CHANGED
@@ -9,7 +9,17 @@ import { buildScopesFromEndpoints } from './auth.js';
9
9
  async function main() {
10
10
  try {
11
11
  const args = parseArgs();
12
- const scopes = buildScopesFromEndpoints(args.forceWorkScopes);
12
+ let includeWorkScopes = args.forceWorkScopes;
13
+ if (!includeWorkScopes) {
14
+ const tempAuthManager = new AuthManager(undefined, buildScopesFromEndpoints(false));
15
+ await tempAuthManager.loadTokenCache();
16
+ const hasWorkPermissions = await tempAuthManager.hasWorkAccountPermissions();
17
+ if (hasWorkPermissions) {
18
+ includeWorkScopes = true;
19
+ logger.info('Detected existing work account permissions, including work scopes');
20
+ }
21
+ }
22
+ const scopes = buildScopesFromEndpoints(includeWorkScopes);
13
23
  const authManager = new AuthManager(undefined, scopes);
14
24
  await authManager.loadTokenCache();
15
25
  if (args.login) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "description": "Microsoft 365 MCP Server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, join } from 'path';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ /**
11
+ * Detect if a schema definition creates recursive references
12
+ * Handles complex recursive paths like #/definitions/X/properties/body/anyOf/1
13
+ *
14
+ * I really really hope this solves
15
+ * https://github.com/Softeria/ms-365-mcp-server/issues/36 and perhaps even
16
+ * https://github.com/Softeria/ms-365-mcp-server/issues/62
17
+ *
18
+ * Or any other silly tool that doesn't support recursive $refs
19
+ *
20
+ * Note - if the tool still struggles with $ref in general, this fix won't help!
21
+ */
22
+ function detectRecursiveRefs(schema, definitionName) {
23
+ if (!schema || typeof schema !== 'object') return [];
24
+
25
+ const recursions = [];
26
+ const currentDefPath = `#/definitions/${definitionName}`;
27
+
28
+ function findAllRefs(obj, path = []) {
29
+ const refs = [];
30
+
31
+ function traverse(current, currentPath) {
32
+ if (!current || typeof current !== 'object') return;
33
+
34
+ if (Array.isArray(current)) {
35
+ current.forEach((item, index) => traverse(item, [...currentPath, index]));
36
+ return;
37
+ }
38
+
39
+ if (current.$ref) {
40
+ refs.push({
41
+ ref: current.$ref,
42
+ path: currentPath.join('.'),
43
+ });
44
+ return;
45
+ }
46
+
47
+ Object.entries(current).forEach(([key, value]) => {
48
+ traverse(value, [...currentPath, key]);
49
+ });
50
+ }
51
+
52
+ traverse(obj, path);
53
+ return refs;
54
+ }
55
+
56
+ const allRefs = findAllRefs(schema);
57
+
58
+ for (const refInfo of allRefs) {
59
+ const ref = refInfo.ref;
60
+
61
+ if (ref.startsWith(currentDefPath)) {
62
+ recursions.push({
63
+ path: refInfo.path,
64
+ ref: ref,
65
+ type: 'recursive_reference',
66
+ });
67
+ } else if (ref === currentDefPath) {
68
+ recursions.push({
69
+ path: refInfo.path,
70
+ ref: ref,
71
+ type: 'direct_self_reference',
72
+ });
73
+ }
74
+ }
75
+
76
+ return recursions;
77
+ }
78
+
79
+ function removeRecursiveProperties(schema, recursions) {
80
+ if (!schema || typeof schema !== 'object' || recursions.length === 0) {
81
+ return schema;
82
+ }
83
+
84
+ const cleaned = JSON.parse(JSON.stringify(schema));
85
+
86
+ const propertiesToRemove = new Set();
87
+
88
+ for (const recursion of recursions) {
89
+ const pathParts = recursion.path.split('.').filter((p) => p !== '');
90
+
91
+ if (pathParts[pathParts.length - 1] === 'items' && pathParts.length > 1) {
92
+ const propertyPath = pathParts.slice(0, -1).join('.');
93
+ propertiesToRemove.add(propertyPath);
94
+ } else {
95
+ propertiesToRemove.add(recursion.path);
96
+ }
97
+ }
98
+
99
+ const sortedPaths = Array.from(propertiesToRemove).sort(
100
+ (a, b) => b.split('.').length - a.split('.').length
101
+ );
102
+
103
+ for (const propertyPath of sortedPaths) {
104
+ const pathParts = propertyPath.split('.');
105
+
106
+ let current = cleaned;
107
+ for (let i = 0; i < pathParts.length - 1; i++) {
108
+ const part = pathParts[i];
109
+ if (current && typeof current === 'object' && part in current) {
110
+ current = current[part];
111
+ } else {
112
+ current = null;
113
+ break;
114
+ }
115
+ }
116
+
117
+ if (current && typeof current === 'object') {
118
+ const propertyName = pathParts[pathParts.length - 1];
119
+ if (propertyName in current) {
120
+ console.log(` Removing recursive property: ${propertyPath}`);
121
+ delete current[propertyName];
122
+ }
123
+ }
124
+ }
125
+
126
+ return cleaned;
127
+ }
128
+
129
+ /**
130
+ * Process a tool to remove recursive references while keeping other $refs
131
+ */
132
+ function processToolSchema(tool) {
133
+ if (!tool.inputSchema || !tool.inputSchema.definitions) {
134
+ return tool;
135
+ }
136
+
137
+ const definitions = tool.inputSchema.definitions;
138
+ const processedDefinitions = {};
139
+ let totalRecursionsRemoved = 0;
140
+
141
+ console.log(`\n🔧 Processing ${tool.name}:`);
142
+
143
+ for (const [defName, defSchema] of Object.entries(definitions)) {
144
+ const recursions = detectRecursiveRefs(defSchema, defName);
145
+
146
+ if (recursions.length > 0) {
147
+ console.log(` Found ${recursions.length} recursive references in ${defName}`);
148
+ processedDefinitions[defName] = removeRecursiveProperties(defSchema, recursions);
149
+ totalRecursionsRemoved += recursions.length;
150
+ } else {
151
+ processedDefinitions[defName] = defSchema;
152
+ }
153
+ }
154
+
155
+ const cleanedTool = {
156
+ ...tool,
157
+ inputSchema: {
158
+ ...tool.inputSchema,
159
+ definitions: processedDefinitions,
160
+ },
161
+ };
162
+
163
+ console.log(` ✂️ Removed ${totalRecursionsRemoved} recursive references`);
164
+ return cleanedTool;
165
+ }
166
+
167
+ async function removeRecursiveRefs() {
168
+ try {
169
+ console.log('✂️ Removing Recursive References (Keeping Other $refs)\n');
170
+ console.log('='.repeat(60));
171
+
172
+ const inputPath = join(__dirname, 'schemas-with-refs-direct.json');
173
+ if (!fs.existsSync(inputPath)) {
174
+ throw new Error('Schema file not found. Run extract-schemas-direct.js first.');
175
+ }
176
+
177
+ const originalData = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
178
+ const tools = originalData.result?.tools || [];
179
+
180
+ console.log(`Processing ${tools.length} tools...`);
181
+
182
+ const processedTools = tools.map(processToolSchema);
183
+
184
+ const cleanedData = {
185
+ ...originalData,
186
+ result: {
187
+ ...originalData.result,
188
+ tools: processedTools,
189
+ },
190
+ };
191
+
192
+ const outputPath = join(__dirname, 'schemas-properties-removed.json');
193
+ const cleanedString = JSON.stringify(cleanedData, null, 2);
194
+ fs.writeFileSync(outputPath, cleanedString);
195
+
196
+ console.log(`\n💾 Cleaned schemas saved to: ${outputPath}`);
197
+
198
+ const originalString = JSON.stringify(originalData);
199
+ const originalRefs = (originalString.match(/\$ref/g) || []).length;
200
+ const cleanedRefs = (cleanedString.match(/\$ref/g) || []).length;
201
+ const removedRefs = originalRefs - cleanedRefs;
202
+
203
+ console.log('\n📊 CLEANING ANALYSIS');
204
+ console.log('-'.repeat(40));
205
+ console.log(
206
+ `Original size: ${originalString.length.toLocaleString()} chars (${(originalString.length / 1024).toFixed(2)} KB)`
207
+ );
208
+ console.log(
209
+ `Cleaned size: ${cleanedString.length.toLocaleString()} chars (${(cleanedString.length / 1024).toFixed(2)} KB)`
210
+ );
211
+
212
+ const sizeDiff = cleanedString.length - originalString.length;
213
+ const sizeChange = ((sizeDiff / originalString.length) * 100).toFixed(1);
214
+ console.log(`Size change: ${sizeDiff.toLocaleString()} chars (${sizeChange}%)`);
215
+
216
+ console.log(`\nOriginal $refs: ${originalRefs}`);
217
+ console.log(`Cleaned $refs: ${cleanedRefs}`);
218
+ console.log(`Removed $refs: ${removedRefs}`);
219
+ console.log(`Refs remaining: ${((cleanedRefs / originalRefs) * 100).toFixed(1)}%`);
220
+
221
+ console.log('\n🧪 QUICK RECURSION CHECK');
222
+ console.log('-'.repeat(40));
223
+
224
+ const sampleRecursiveTools = [
225
+ 'create-calendar-event',
226
+ 'update-calendar-event',
227
+ 'create-onenote-page',
228
+ ];
229
+ let foundRecursions = 0;
230
+
231
+ for (const toolName of sampleRecursiveTools) {
232
+ const tool = processedTools.find((t) => t.name === toolName);
233
+ if (tool) {
234
+ const toolString = JSON.stringify(tool);
235
+ const recursivePattern = `#/definitions/${toolName}Parameters/properties/body/anyOf/1`;
236
+ if (toolString.includes(recursivePattern)) {
237
+ foundRecursions++;
238
+ console.log(` ❌ ${toolName}: Still contains recursive pattern`);
239
+ } else {
240
+ console.log(` ✅ ${toolName}: Recursive pattern removed`);
241
+ }
242
+ }
243
+ }
244
+
245
+ if (foundRecursions === 0) {
246
+ console.log('\n✅ No recursive patterns found in sample tools!');
247
+ } else {
248
+ console.log(`\n⚠️ ${foundRecursions} tools still contain recursive patterns`);
249
+ }
250
+
251
+ console.log('\n💡 SUMMARY');
252
+ console.log('='.repeat(40));
253
+
254
+ if (removedRefs > 0) {
255
+ console.log(`✅ Successfully removed ${removedRefs} recursive references`);
256
+ console.log(`✅ Kept ${cleanedRefs} non-recursive $ref references`);
257
+
258
+ if (sizeDiff < 0) {
259
+ console.log(`✅ Reduced schema size by ${Math.abs(sizeDiff).toLocaleString()} characters`);
260
+ } else if (sizeDiff < originalString.length * 0.1) {
261
+ console.log(`✅ Minimal size increase (${sizeChange}%)`);
262
+ }
263
+
264
+ console.log('\n📋 BENEFITS:');
265
+ console.log('• Eliminates infinite recursion during flattening');
266
+ console.log('• Preserves beneficial $ref references for shared types');
267
+ console.log('• May allow partial flattening for LangChain compatibility');
268
+ console.log('• Reduces schema complexity while maintaining functionality');
269
+ } else {
270
+ console.log('ℹ️ No recursive references found to remove');
271
+ }
272
+
273
+ const remainingRefTypes = new Set();
274
+ cleanedString.match(/"#\/definitions\/[^"]+"/g)?.forEach((ref) => {
275
+ const defName = ref.split('/').pop()?.replace('"', '');
276
+ if (defName) remainingRefTypes.add(defName);
277
+ });
278
+
279
+ console.log(`\n🔗 Remaining reference types: ${remainingRefTypes.size}`);
280
+ if (remainingRefTypes.size <= 10) {
281
+ console.log('Sample remaining refs:', Array.from(remainingRefTypes).slice(0, 5).join(', '));
282
+ }
283
+ } catch (error) {
284
+ console.error('Error removing recursive refs:', error.message);
285
+ console.error(error.stack);
286
+ process.exit(1);
287
+ }
288
+ }
289
+
290
+ export { removeRecursiveRefs };
291
+
292
+ if (import.meta.url === `file://${process.argv[1]}`) {
293
+ removeRecursiveRefs();
294
+ }