@softeria/ms-365-mcp-server 0.10.1 โ†’ 0.11.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/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.0",
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
+ }