@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 +2 -1
- package/dist/graph-client.js +20 -2
- package/dist/index.js +11 -1
- package/package.json +1 -1
- package/remove-recursive-refs.js +294 -0
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(`
|
|
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;
|
package/dist/graph-client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
+
}
|