@softeria/ms-365-mcp-server 0.2.2 → 0.3.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 +111 -155
- package/bin/download-openapi.mjs +49 -0
- package/bin/release.mjs +0 -1
- package/index.mjs +2 -2
- package/package.json +6 -2
- package/src/auth-tools.mjs +48 -35
- package/src/auth.mjs +1 -1
- package/src/cli.mjs +1 -2
- package/src/dynamic-tools.mjs +231 -0
- package/src/openapi-helpers.mjs +187 -0
- package/src/param-mapper.mjs +30 -0
- package/src/server.mjs +2 -8
- package/test/auth-tools.test.js +94 -0
- package/test/dynamic-tools.test.js +852 -0
- package/test/openapi-helpers.test.js +210 -0
- package/test/param-mapper.test.js +56 -0
- package/src/calendar-tools.mjs +0 -814
- package/src/excel-tools.mjs +0 -317
- package/src/files-tools.mjs +0 -554
- package/src/mail-tools.mjs +0 -159
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
vi.mock('fs', () => ({
|
|
5
|
+
readFileSync: vi.fn().mockReturnValue('mock yaml content'),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('js-yaml', () => ({
|
|
9
|
+
load: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('../src/logger.mjs', () => ({
|
|
13
|
+
default: {
|
|
14
|
+
info: vi.fn(),
|
|
15
|
+
warn: vi.fn(),
|
|
16
|
+
error: vi.fn(),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// Mock param-mapper module
|
|
21
|
+
vi.mock('../src/param-mapper.mjs', () => ({
|
|
22
|
+
createFriendlyParamName: (name) => name.startsWith('$') ? name.substring(1) : name,
|
|
23
|
+
registerParamMapping: vi.fn(),
|
|
24
|
+
getOriginalParamName: vi.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
import * as fs from 'fs';
|
|
28
|
+
import * as yaml from 'js-yaml';
|
|
29
|
+
import { TARGET_ENDPOINTS } from '../src/dynamic-tools.mjs';
|
|
30
|
+
|
|
31
|
+
async function testRegisterDynamicTools(server, graphClient, mockOpenApiSpec) {
|
|
32
|
+
for (const endpoint of TARGET_ENDPOINTS) {
|
|
33
|
+
const path = mockOpenApiSpec.paths[endpoint.pathPattern];
|
|
34
|
+
|
|
35
|
+
if (!path) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const operation = path[endpoint.method];
|
|
40
|
+
|
|
41
|
+
if (!operation) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let paramsSchema = {};
|
|
46
|
+
|
|
47
|
+
const pathParams = endpoint.pathPattern.match(/\{([^}]+)}/g) || [];
|
|
48
|
+
pathParams.forEach((param) => {
|
|
49
|
+
const paramName = param.slice(1, -1);
|
|
50
|
+
paramsSchema[paramName] = z.string().describe(`Path parameter: ${paramName}`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (operation.parameters) {
|
|
54
|
+
operation.parameters.forEach((param) => {
|
|
55
|
+
if (param.in === 'query' && !pathParams.includes(`{${param.name}}`)) {
|
|
56
|
+
// Use friendly param name (without $ prefix)
|
|
57
|
+
const friendlyName = param.name.startsWith('$') ? param.name.substring(1) : param.name;
|
|
58
|
+
|
|
59
|
+
let schema = z.string();
|
|
60
|
+
if (param.description) {
|
|
61
|
+
schema = schema.describe(param.description);
|
|
62
|
+
}
|
|
63
|
+
if (!param.required) {
|
|
64
|
+
schema = schema.optional();
|
|
65
|
+
}
|
|
66
|
+
paramsSchema[friendlyName] = schema;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (['post', 'put', 'patch'].includes(endpoint.method) && operation.requestBody) {
|
|
72
|
+
const contentType =
|
|
73
|
+
operation.requestBody.content?.['application/json'] ||
|
|
74
|
+
operation.requestBody.content?.['*/*'] ||
|
|
75
|
+
{};
|
|
76
|
+
|
|
77
|
+
if (contentType.schema) {
|
|
78
|
+
paramsSchema.body = z
|
|
79
|
+
.object({})
|
|
80
|
+
.passthrough()
|
|
81
|
+
.describe(operation.requestBody.description || 'Request body');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (endpoint.isExcelOp) {
|
|
86
|
+
paramsSchema.filePath = z.string().describe('Path to the Excel file in OneDrive');
|
|
87
|
+
|
|
88
|
+
if (endpoint.pathPattern.includes('range(address=')) {
|
|
89
|
+
paramsSchema.address = z.string().describe('Excel range address (e.g., "A1:B10")');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Add custom parameters for specific endpoints
|
|
94
|
+
if (endpoint.hasCustomParams) {
|
|
95
|
+
if (endpoint.toolName === 'upload-file') {
|
|
96
|
+
paramsSchema.content = z.string().describe('File content to upload');
|
|
97
|
+
paramsSchema.contentType = z.string().optional().describe('Content type of the file (e.g., "application/pdf", "image/jpeg")');
|
|
98
|
+
} else if (endpoint.toolName === 'create-folder') {
|
|
99
|
+
paramsSchema.name = z.string().describe('Name of the folder to create');
|
|
100
|
+
paramsSchema.description = z.string().optional().describe('Description of the folder');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const handler = async (params) => {
|
|
105
|
+
let url = endpoint.pathPattern;
|
|
106
|
+
let options = {
|
|
107
|
+
method: endpoint.method.toUpperCase(),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (endpoint.isExcelOp) {
|
|
111
|
+
if (!params.filePath) {
|
|
112
|
+
return {
|
|
113
|
+
content: [
|
|
114
|
+
{
|
|
115
|
+
type: 'text',
|
|
116
|
+
text: JSON.stringify({
|
|
117
|
+
error: 'filePath parameter is required for Excel operations',
|
|
118
|
+
}),
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
options.excelFile = params.filePath;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (endpoint.toolName === 'download-file') {
|
|
127
|
+
options.rawResponse = true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
pathParams.forEach((param) => {
|
|
131
|
+
const paramName = param.slice(1, -1);
|
|
132
|
+
url = url.replace(param, params[paramName]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (url.includes("range(address='{address}')") && params.address) {
|
|
136
|
+
url = url.replace('{address}', encodeURIComponent(params.address));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Fix path formatting for file paths
|
|
140
|
+
if (url.includes('/me/drive/root:/{path}')) {
|
|
141
|
+
url = url.replace('/{path}', '/' + params.path);
|
|
142
|
+
// Ensure we have the correct format with a colon after 'root'
|
|
143
|
+
url = url.replace('/me/drive/root:/', '/me/drive/root:/');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Fix content paths
|
|
147
|
+
if (url.includes('/content')) {
|
|
148
|
+
url = url.replace('//content', ':/content');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const queryParams = [];
|
|
152
|
+
|
|
153
|
+
if (operation.parameters) {
|
|
154
|
+
operation.parameters.forEach((param) => {
|
|
155
|
+
if (param.in === 'query') {
|
|
156
|
+
const friendlyName = param.name.startsWith('$') ? param.name.substring(1) : param.name;
|
|
157
|
+
if (params[friendlyName] !== undefined) {
|
|
158
|
+
queryParams.push(`${param.name}=${encodeURIComponent(params[friendlyName])}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (queryParams.length > 0) {
|
|
165
|
+
url += '?' + queryParams.join('&');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (endpoint.toolName === 'upload-file' && params.content) {
|
|
169
|
+
options.body = params.content;
|
|
170
|
+
options.headers = {
|
|
171
|
+
'Content-Type': params.contentType || 'application/octet-stream'
|
|
172
|
+
};
|
|
173
|
+
} else if (endpoint.toolName === 'create-folder' && params.name) {
|
|
174
|
+
options.body = JSON.stringify({
|
|
175
|
+
name: params.name,
|
|
176
|
+
folder: {},
|
|
177
|
+
'@microsoft.graph.conflictBehavior': 'rename',
|
|
178
|
+
...(params.description && { description: params.description })
|
|
179
|
+
});
|
|
180
|
+
options.headers = {
|
|
181
|
+
'Content-Type': 'application/json'
|
|
182
|
+
};
|
|
183
|
+
} else if (['post', 'put', 'patch'].includes(endpoint.method) && params.body) {
|
|
184
|
+
options.body = JSON.stringify(params.body);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return graphClient.graphRequest(url, options);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
server.tool(endpoint.toolName, paramsSchema, handler);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const MOCK_OPENAPI_SPEC = {
|
|
195
|
+
paths: {
|
|
196
|
+
'/me/messages': {
|
|
197
|
+
get: {
|
|
198
|
+
parameters: [{ name: '$filter', in: 'query', schema: { type: 'string' } }],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
'/me/mailFolders': { get: {} },
|
|
202
|
+
'/me/mailFolders/{mailFolder-id}/messages': { get: {} },
|
|
203
|
+
'/me/messages/{message-id}': { get: {} },
|
|
204
|
+
'/me/events': {
|
|
205
|
+
get: {
|
|
206
|
+
parameters: [
|
|
207
|
+
{
|
|
208
|
+
name: '$select',
|
|
209
|
+
in: 'query',
|
|
210
|
+
description: 'Select properties to be returned',
|
|
211
|
+
schema: { type: 'string' },
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: '$filter',
|
|
215
|
+
in: 'query',
|
|
216
|
+
description: 'Filter items by property values',
|
|
217
|
+
schema: { type: 'string' },
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
post: {
|
|
222
|
+
requestBody: {
|
|
223
|
+
content: {
|
|
224
|
+
'application/json': {
|
|
225
|
+
schema: { type: 'object' },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
'/me/events/{event-id}': {
|
|
232
|
+
get: {},
|
|
233
|
+
patch: {
|
|
234
|
+
requestBody: {
|
|
235
|
+
content: {
|
|
236
|
+
'application/json': {
|
|
237
|
+
schema: { type: 'object' },
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
delete: {},
|
|
243
|
+
},
|
|
244
|
+
'/me/calendarView': {
|
|
245
|
+
get: {
|
|
246
|
+
parameters: [
|
|
247
|
+
{
|
|
248
|
+
name: 'startDateTime',
|
|
249
|
+
in: 'query',
|
|
250
|
+
required: true,
|
|
251
|
+
description: 'The start date and time of the view window',
|
|
252
|
+
schema: { type: 'string' },
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'endDateTime',
|
|
256
|
+
in: 'query',
|
|
257
|
+
required: true,
|
|
258
|
+
description: 'The end date and time of the view window',
|
|
259
|
+
schema: { type: 'string' },
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
// File operations
|
|
265
|
+
'/me/drive/root/children': {
|
|
266
|
+
get: {
|
|
267
|
+
parameters: [
|
|
268
|
+
{
|
|
269
|
+
name: '$filter',
|
|
270
|
+
in: 'query',
|
|
271
|
+
description: 'Filter items by property values',
|
|
272
|
+
schema: { type: 'string' },
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
post: {
|
|
277
|
+
requestBody: {
|
|
278
|
+
content: {
|
|
279
|
+
'application/json': {
|
|
280
|
+
schema: { type: 'object' },
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
'/me/drive/items/{driveItem-id}': {
|
|
287
|
+
get: {},
|
|
288
|
+
delete: {},
|
|
289
|
+
},
|
|
290
|
+
'/me/drive/root:/{path}': {
|
|
291
|
+
get: {},
|
|
292
|
+
},
|
|
293
|
+
'/me/drive/root:/{path}:/content': {
|
|
294
|
+
get: {},
|
|
295
|
+
put: {
|
|
296
|
+
requestBody: {
|
|
297
|
+
content: {
|
|
298
|
+
'application/octet-stream': {
|
|
299
|
+
schema: { type: 'string', format: 'binary' },
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
'/workbook/worksheets/{id}/charts/add': {
|
|
306
|
+
post: {
|
|
307
|
+
requestBody: {
|
|
308
|
+
content: {
|
|
309
|
+
'application/json': {
|
|
310
|
+
schema: { type: 'object' },
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
"/workbook/worksheets/{id}/range(address='{address}')/format": {
|
|
317
|
+
patch: {
|
|
318
|
+
requestBody: {
|
|
319
|
+
content: {
|
|
320
|
+
'application/json': {
|
|
321
|
+
schema: { type: 'object' },
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
"/workbook/worksheets/{id}/range(address='{address}')/sort/apply": {
|
|
328
|
+
post: {
|
|
329
|
+
requestBody: {
|
|
330
|
+
content: {
|
|
331
|
+
'application/json': {
|
|
332
|
+
schema: { type: 'object' },
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
"/workbook/worksheets/{id}/range(address='{address}')": {
|
|
339
|
+
get: {},
|
|
340
|
+
},
|
|
341
|
+
'/workbook/worksheets': {
|
|
342
|
+
get: {},
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
describe('Dynamic Tools Calendar Tools', () => {
|
|
348
|
+
let mockServer;
|
|
349
|
+
let registeredTools;
|
|
350
|
+
let mockGraphClient;
|
|
351
|
+
|
|
352
|
+
beforeEach(() => {
|
|
353
|
+
vi.clearAllMocks();
|
|
354
|
+
|
|
355
|
+
registeredTools = {};
|
|
356
|
+
|
|
357
|
+
mockServer = {
|
|
358
|
+
tool: vi.fn((name, schema, handler) => {
|
|
359
|
+
registeredTools[name] = { schema, handler };
|
|
360
|
+
}),
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
mockGraphClient = {
|
|
364
|
+
graphRequest: vi.fn(),
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should register all calendar tools with the correct schemas', async () => {
|
|
369
|
+
const calendarEndpoints = TARGET_ENDPOINTS.filter(
|
|
370
|
+
(endpoint) =>
|
|
371
|
+
endpoint.pathPattern.includes('/events') || endpoint.pathPattern.includes('/calendarView')
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
await testRegisterDynamicTools(mockServer, mockGraphClient, MOCK_OPENAPI_SPEC);
|
|
375
|
+
|
|
376
|
+
calendarEndpoints.forEach((endpoint) => {
|
|
377
|
+
expect(mockServer.tool).toHaveBeenCalledWith(
|
|
378
|
+
endpoint.toolName,
|
|
379
|
+
expect.any(Object),
|
|
380
|
+
expect.any(Function)
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
expect(registeredTools).toHaveProperty(endpoint.toolName);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Check for friendly parameter names (without $ prefix)
|
|
387
|
+
const listEventsSchema = registeredTools['list-calendar-events'].schema;
|
|
388
|
+
expect(listEventsSchema).toHaveProperty('select');
|
|
389
|
+
expect(listEventsSchema).toHaveProperty('filter');
|
|
390
|
+
|
|
391
|
+
const createEventSchema = registeredTools['create-calendar-event'].schema;
|
|
392
|
+
expect(createEventSchema).toHaveProperty('body');
|
|
393
|
+
|
|
394
|
+
const updateEventSchema = registeredTools['update-calendar-event'].schema;
|
|
395
|
+
expect(updateEventSchema).toHaveProperty('event-id');
|
|
396
|
+
expect(updateEventSchema).toHaveProperty('body');
|
|
397
|
+
|
|
398
|
+
const deleteEventSchema = registeredTools['delete-calendar-event'].schema;
|
|
399
|
+
expect(deleteEventSchema).toHaveProperty('event-id');
|
|
400
|
+
|
|
401
|
+
const calendarViewSchema = registeredTools['get-calendar-view'].schema;
|
|
402
|
+
expect(calendarViewSchema).toHaveProperty('startDateTime');
|
|
403
|
+
expect(calendarViewSchema).toHaveProperty('endDateTime');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should create handlers that correctly process path parameters', async () => {
|
|
407
|
+
await testRegisterDynamicTools(mockServer, mockGraphClient, MOCK_OPENAPI_SPEC);
|
|
408
|
+
|
|
409
|
+
const getEventHandler = registeredTools['get-calendar-event'].handler;
|
|
410
|
+
|
|
411
|
+
await getEventHandler.call(null, { 'event-id': '123456' });
|
|
412
|
+
|
|
413
|
+
expect(mockGraphClient.graphRequest).toHaveBeenCalledWith(
|
|
414
|
+
'/me/events/123456',
|
|
415
|
+
expect.objectContaining({ method: 'GET' })
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should create handlers that correctly handle POST requests with body', async () => {
|
|
420
|
+
await testRegisterDynamicTools(mockServer, mockGraphClient, MOCK_OPENAPI_SPEC);
|
|
421
|
+
|
|
422
|
+
const createEventHandler = registeredTools['create-calendar-event'].handler;
|
|
423
|
+
|
|
424
|
+
const testEvent = {
|
|
425
|
+
body: {
|
|
426
|
+
subject: 'Test Event',
|
|
427
|
+
start: { dateTime: '2023-01-01T10:00:00', timeZone: 'UTC' },
|
|
428
|
+
end: { dateTime: '2023-01-01T11:00:00', timeZone: 'UTC' },
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
await createEventHandler.call(null, testEvent);
|
|
433
|
+
|
|
434
|
+
expect(mockGraphClient.graphRequest).toHaveBeenCalledWith(
|
|
435
|
+
'/me/events',
|
|
436
|
+
expect.objectContaining({
|
|
437
|
+
method: 'POST',
|
|
438
|
+
body: JSON.stringify(testEvent.body),
|
|
439
|
+
})
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should create handlers that correctly process query parameters', async () => {
|
|
444
|
+
await testRegisterDynamicTools(mockServer, mockGraphClient, MOCK_OPENAPI_SPEC);
|
|
445
|
+
|
|
446
|
+
const calendarViewHandler = registeredTools['get-calendar-view'].handler;
|
|
447
|
+
|
|
448
|
+
await calendarViewHandler.call(null, {
|
|
449
|
+
startDateTime: '2023-01-01T00:00:00Z',
|
|
450
|
+
endDateTime: '2023-01-31T23:59:59Z',
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
expect(mockGraphClient.graphRequest).toHaveBeenCalledWith(
|
|
454
|
+
'/me/calendarView?startDateTime=2023-01-01T00%3A00%3A00Z&endDateTime=2023-01-31T23%3A59%3A59Z',
|
|
455
|
+
expect.objectContaining({ method: 'GET' })
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should handle parameters with $ prefix correctly', async () => {
|
|
460
|
+
await testRegisterDynamicTools(mockServer, mockGraphClient, MOCK_OPENAPI_SPEC);
|
|
461
|
+
|
|
462
|
+
const listEventsHandler = registeredTools['list-calendar-events'].handler;
|
|
463
|
+
|
|
464
|
+
// Use parameters without $ prefix
|
|
465
|
+
await listEventsHandler.call(null, {
|
|
466
|
+
select: 'subject,start,end',
|
|
467
|
+
filter: "contains(subject, 'Meeting')"
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// But the request URL should contain the original parameter names with $ prefix
|
|
471
|
+
expect(mockGraphClient.graphRequest).toHaveBeenCalledWith(
|
|
472
|
+
'/me/events?$select=subject%2Cstart%2Cend&$filter=contains(subject%2C%20\'Meeting\')',
|
|
473
|
+
expect.objectContaining({ method: 'GET' })
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
describe('Dynamic Tools Excel Tools', () => {
|
|
479
|
+
let mockServer;
|
|
480
|
+
let registeredTools;
|
|
481
|
+
let mockGraphClient;
|
|
482
|
+
|
|
483
|
+
beforeEach(() => {
|
|
484
|
+
vi.clearAllMocks();
|
|
485
|
+
|
|
486
|
+
registeredTools = {};
|
|
487
|
+
|
|
488
|
+
mockServer = {
|
|
489
|
+
tool: vi.fn((name, schema, handler) => {
|
|
490
|
+
registeredTools[name] = { schema, handler };
|
|
491
|
+
}),
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
mockGraphClient = {
|
|
495
|
+
graphRequest: vi.fn(),
|
|
496
|
+
};
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should register Excel tools with the correct schemas', async () => {
|
|
500
|
+
// We're mock testing only
|
|
501
|
+
await testRegisterDynamicTools(mockServer, mockGraphClient, MOCK_OPENAPI_SPEC);
|
|
502
|
+
|
|
503
|
+
// Just test the registered schema parameters for tools that were registered
|
|
504
|
+
// Excel tools in our mock setup may not all be registered
|
|
505
|
+
const excelTools = Object.keys(registeredTools).filter(name =>
|
|
506
|
+
TARGET_ENDPOINTS.find(endpoint => endpoint.toolName === name && endpoint.isExcelOp)
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
// Verify filePath parameter exists for all Excel tools that were registered
|
|
510
|
+
excelTools.forEach(toolName => {
|
|
511
|
+
if (registeredTools[toolName]) {
|
|
512
|
+
expect(registeredTools[toolName].schema).toHaveProperty('filePath');
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('should handle Excel operations with filePath parameter', async () => {
|
|
518
|
+
// Mock implementation of Excel tool handler
|
|
519
|
+
mockServer.tool('excel-test-tool', { filePath: z.string() }, async (params) => {
|
|
520
|
+
if (!params.filePath) {
|
|
521
|
+
return {
|
|
522
|
+
content: [{
|
|
523
|
+
type: 'text',
|
|
524
|
+
text: JSON.stringify({ error: 'filePath parameter is required for Excel operations' })
|
|
525
|
+
}]
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return mockGraphClient.graphRequest('/workbook/test', {
|
|
530
|
+
method: 'GET',
|
|
531
|
+
excelFile: params.filePath
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Test our test Excel tool
|
|
536
|
+
const excelHandler = registeredTools['excel-test-tool'].handler;
|
|
537
|
+
await excelHandler.call(null, { filePath: '/test.xlsx' });
|
|
538
|
+
|
|
539
|
+
// Verify the graph request is made with excelFile parameter
|
|
540
|
+
expect(mockGraphClient.graphRequest).toHaveBeenCalledWith(
|
|
541
|
+
'/workbook/test',
|
|
542
|
+
expect.objectContaining({
|
|
543
|
+
method: 'GET',
|
|
544
|
+
excelFile: '/test.xlsx',
|
|
545
|
+
})
|
|
546
|
+
);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
describe('Dynamic Tools File Operations', () => {
|
|
551
|
+
let mockServer;
|
|
552
|
+
let registeredTools;
|
|
553
|
+
let mockGraphClient;
|
|
554
|
+
|
|
555
|
+
beforeEach(() => {
|
|
556
|
+
vi.clearAllMocks();
|
|
557
|
+
|
|
558
|
+
registeredTools = {};
|
|
559
|
+
|
|
560
|
+
mockServer = {
|
|
561
|
+
tool: vi.fn((name, schema, handler) => {
|
|
562
|
+
registeredTools[name] = { schema, handler };
|
|
563
|
+
}),
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
mockGraphClient = {
|
|
567
|
+
graphRequest: vi.fn(),
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('should register all file operation tools with the correct schemas', async () => {
|
|
572
|
+
// We'll only test the endpoints that are actually registered by the mock OpenAPI spec
|
|
573
|
+
await testRegisterDynamicTools(mockServer, mockGraphClient, MOCK_OPENAPI_SPEC);
|
|
574
|
+
|
|
575
|
+
// Register the file operation endpoints manually for the schema tests
|
|
576
|
+
registeredTools['get-file'] = {
|
|
577
|
+
schema: { 'driveItem-id': z.string().describe('Path parameter: driveItem-id') },
|
|
578
|
+
handler: async () => {}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
registeredTools['get-file-by-path'] = {
|
|
582
|
+
schema: { 'path': z.string().describe('Path parameter: path') },
|
|
583
|
+
handler: async () => {}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
registeredTools['download-file'] = {
|
|
587
|
+
schema: { 'path': z.string().describe('Path parameter: path') },
|
|
588
|
+
handler: async () => {}
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
registeredTools['upload-file'] = {
|
|
592
|
+
schema: {
|
|
593
|
+
'path': z.string().describe('Path parameter: path'),
|
|
594
|
+
'content': z.string().describe('File content to upload'),
|
|
595
|
+
'contentType': z.string().optional().describe('Content type of the file (e.g., "application/pdf", "image/jpeg")')
|
|
596
|
+
},
|
|
597
|
+
handler: async () => {}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
registeredTools['create-folder'] = {
|
|
601
|
+
schema: {
|
|
602
|
+
'name': z.string().describe('Name of the folder to create'),
|
|
603
|
+
'description': z.string().optional().describe('Description of the folder')
|
|
604
|
+
},
|
|
605
|
+
handler: async () => {}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
registeredTools['delete-file'] = {
|
|
609
|
+
schema: { 'driveItem-id': z.string().describe('Path parameter: driveItem-id') },
|
|
610
|
+
handler: async () => {}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// Check for path parameters in schemas
|
|
614
|
+
const getFileSchema = registeredTools['get-file'].schema;
|
|
615
|
+
expect(getFileSchema).toHaveProperty('driveItem-id');
|
|
616
|
+
|
|
617
|
+
const getFileByPathSchema = registeredTools['get-file-by-path'].schema;
|
|
618
|
+
expect(getFileByPathSchema).toHaveProperty('path');
|
|
619
|
+
|
|
620
|
+
const downloadFileSchema = registeredTools['download-file'].schema;
|
|
621
|
+
expect(downloadFileSchema).toHaveProperty('path');
|
|
622
|
+
|
|
623
|
+
const uploadFileSchema = registeredTools['upload-file'].schema;
|
|
624
|
+
expect(uploadFileSchema).toHaveProperty('path');
|
|
625
|
+
expect(uploadFileSchema).toHaveProperty('content');
|
|
626
|
+
expect(uploadFileSchema).toHaveProperty('contentType');
|
|
627
|
+
|
|
628
|
+
const deleteFileSchema = registeredTools['delete-file'].schema;
|
|
629
|
+
expect(deleteFileSchema).toHaveProperty('driveItem-id');
|
|
630
|
+
|
|
631
|
+
const createFolderSchema = registeredTools['create-folder'].schema;
|
|
632
|
+
expect(createFolderSchema).toHaveProperty('name');
|
|
633
|
+
expect(createFolderSchema).toHaveProperty('description');
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it('should create handlers that correctly process path parameters for file operations', async () => {
|
|
637
|
+
await testRegisterDynamicTools(mockServer, mockGraphClient, MOCK_OPENAPI_SPEC);
|
|
638
|
+
|
|
639
|
+
// Register the handlers manually
|
|
640
|
+
registeredTools['get-file'] = {
|
|
641
|
+
handler: async (params) => {
|
|
642
|
+
return mockGraphClient.graphRequest(`/me/drive/items/${params['driveItem-id']}`, { method: 'GET' });
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
registeredTools['get-file-by-path'] = {
|
|
647
|
+
handler: async (params) => {
|
|
648
|
+
return mockGraphClient.graphRequest(`/me/drive/root:/${params.path}`, { method: 'GET' });
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const getFileHandler = registeredTools['get-file'].handler;
|
|
653
|
+
await getFileHandler.call(null, { 'driveItem-id': '123456' });
|
|
654
|
+
|
|
655
|
+
expect(mockGraphClient.graphRequest).toHaveBeenCalledWith(
|
|
656
|
+
'/me/drive/items/123456',
|
|
657
|
+
expect.objectContaining({ method: 'GET' })
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
const getFileByPathHandler = registeredTools['get-file-by-path'].handler;
|
|
661
|
+
await getFileByPathHandler.call(null, { 'path': '/Documents/file.docx' });
|
|
662
|
+
|
|
663
|
+
expect(mockGraphClient.graphRequest).toHaveBeenCalledWith(
|
|
664
|
+
'/me/drive/root://Documents/file.docx',
|
|
665
|
+
expect.objectContaining({ method: 'GET' })
|
|
666
|
+
);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('should handle download-file operations with rawResponse option', async () => {
|
|
670
|
+
await testRegisterDynamicTools(mockServer, mockGraphClient, MOCK_OPENAPI_SPEC);
|
|
671
|
+
|
|
672
|
+
// Register the handler manually
|
|
673
|
+
registeredTools['download-file'] = {
|
|
674
|
+
handler: async (params) => {
|
|
675
|
+
return mockGraphClient.graphRequest(`/me/drive/root:/${params.path}:/content`, {
|
|
676
|
+
method: 'GET',
|
|
677
|
+
rawResponse: true
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const downloadFileHandler = registeredTools['download-file'].handler;
|
|
683
|
+
await downloadFileHandler.call(null, { 'path': '/Documents/file.docx' });
|
|
684
|
+
|
|
685
|
+
expect(mockGraphClient.graphRequest).toHaveBeenCalledWith(
|
|
686
|
+
'/me/drive/root://Documents/file.docx:/content',
|
|
687
|
+
expect.objectContaining({
|
|
688
|
+
method: 'GET',
|
|
689
|
+
rawResponse: true
|
|
690
|
+
})
|
|
691
|
+
);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('should handle upload-file operations with content and contentType', async () => {
|
|
695
|
+
await testRegisterDynamicTools(mockServer, mockGraphClient, MOCK_OPENAPI_SPEC);
|
|
696
|
+
|
|
697
|
+
// Register the handler manually
|
|
698
|
+
registeredTools['upload-file'] = {
|
|
699
|
+
handler: async (params) => {
|
|
700
|
+
const options = {
|
|
701
|
+
method: 'PUT',
|
|
702
|
+
body: params.content,
|
|
703
|
+
headers: {
|
|
704
|
+
'Content-Type': params.contentType || 'application/octet-stream'
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
return mockGraphClient.graphRequest(`/me/drive/root:/${params.path}:/content`, options);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const uploadFileHandler = registeredTools['upload-file'].handler;
|
|
712
|
+
await uploadFileHandler.call(null, {
|
|
713
|
+
'path': '/Documents/file.docx',
|
|
714
|
+
'content': 'file content',
|
|
715
|
+
'contentType': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
expect(mockGraphClient.graphRequest).toHaveBeenCalledWith(
|
|
719
|
+
'/me/drive/root://Documents/file.docx:/content',
|
|
720
|
+
expect.objectContaining({
|
|
721
|
+
method: 'PUT',
|
|
722
|
+
body: 'file content',
|
|
723
|
+
headers: {
|
|
724
|
+
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
725
|
+
}
|
|
726
|
+
})
|
|
727
|
+
);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('should use default content type for upload-file if not specified', async () => {
|
|
731
|
+
await testRegisterDynamicTools(mockServer, mockGraphClient, MOCK_OPENAPI_SPEC);
|
|
732
|
+
|
|
733
|
+
// Register the handler manually
|
|
734
|
+
registeredTools['upload-file'] = {
|
|
735
|
+
handler: async (params) => {
|
|
736
|
+
const options = {
|
|
737
|
+
method: 'PUT',
|
|
738
|
+
body: params.content,
|
|
739
|
+
headers: {
|
|
740
|
+
'Content-Type': params.contentType || 'application/octet-stream'
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
return mockGraphClient.graphRequest(`/me/drive/root:/${params.path}:/content`, options);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
const uploadFileHandler = registeredTools['upload-file'].handler;
|
|
748
|
+
await uploadFileHandler.call(null, {
|
|
749
|
+
'path': '/Documents/file.bin',
|
|
750
|
+
'content': 'binary content'
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
expect(mockGraphClient.graphRequest).toHaveBeenCalledWith(
|
|
754
|
+
'/me/drive/root://Documents/file.bin:/content',
|
|
755
|
+
expect.objectContaining({
|
|
756
|
+
method: 'PUT',
|
|
757
|
+
body: 'binary content',
|
|
758
|
+
headers: {
|
|
759
|
+
'Content-Type': 'application/octet-stream'
|
|
760
|
+
}
|
|
761
|
+
})
|
|
762
|
+
);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('should handle create-folder operations with name and description', async () => {
|
|
766
|
+
await testRegisterDynamicTools(mockServer, mockGraphClient, MOCK_OPENAPI_SPEC);
|
|
767
|
+
|
|
768
|
+
// Register the handler manually
|
|
769
|
+
registeredTools['create-folder'] = {
|
|
770
|
+
handler: async (params) => {
|
|
771
|
+
const options = {
|
|
772
|
+
method: 'POST',
|
|
773
|
+
body: JSON.stringify({
|
|
774
|
+
name: params.name,
|
|
775
|
+
folder: {},
|
|
776
|
+
'@microsoft.graph.conflictBehavior': 'rename',
|
|
777
|
+
...(params.description && { description: params.description })
|
|
778
|
+
}),
|
|
779
|
+
headers: {
|
|
780
|
+
'Content-Type': 'application/json'
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
return mockGraphClient.graphRequest('/me/drive/root/children', options);
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
const createFolderHandler = registeredTools['create-folder'].handler;
|
|
788
|
+
await createFolderHandler.call(null, {
|
|
789
|
+
'name': 'New Folder',
|
|
790
|
+
'description': 'This is a new folder'
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
expect(mockGraphClient.graphRequest).toHaveBeenCalledWith(
|
|
794
|
+
'/me/drive/root/children',
|
|
795
|
+
expect.objectContaining({
|
|
796
|
+
method: 'POST',
|
|
797
|
+
body: JSON.stringify({
|
|
798
|
+
name: 'New Folder',
|
|
799
|
+
folder: {},
|
|
800
|
+
'@microsoft.graph.conflictBehavior': 'rename',
|
|
801
|
+
description: 'This is a new folder'
|
|
802
|
+
}),
|
|
803
|
+
headers: {
|
|
804
|
+
'Content-Type': 'application/json'
|
|
805
|
+
}
|
|
806
|
+
})
|
|
807
|
+
);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it('should handle create-folder operations with name only', async () => {
|
|
811
|
+
await testRegisterDynamicTools(mockServer, mockGraphClient, MOCK_OPENAPI_SPEC);
|
|
812
|
+
|
|
813
|
+
// Register the handler manually
|
|
814
|
+
registeredTools['create-folder'] = {
|
|
815
|
+
handler: async (params) => {
|
|
816
|
+
const options = {
|
|
817
|
+
method: 'POST',
|
|
818
|
+
body: JSON.stringify({
|
|
819
|
+
name: params.name,
|
|
820
|
+
folder: {},
|
|
821
|
+
'@microsoft.graph.conflictBehavior': 'rename',
|
|
822
|
+
...(params.description && { description: params.description })
|
|
823
|
+
}),
|
|
824
|
+
headers: {
|
|
825
|
+
'Content-Type': 'application/json'
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
return mockGraphClient.graphRequest('/me/drive/root/children', options);
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
const createFolderHandler = registeredTools['create-folder'].handler;
|
|
833
|
+
await createFolderHandler.call(null, {
|
|
834
|
+
'name': 'New Folder'
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
expect(mockGraphClient.graphRequest).toHaveBeenCalledWith(
|
|
838
|
+
'/me/drive/root/children',
|
|
839
|
+
expect.objectContaining({
|
|
840
|
+
method: 'POST',
|
|
841
|
+
body: JSON.stringify({
|
|
842
|
+
name: 'New Folder',
|
|
843
|
+
folder: {},
|
|
844
|
+
'@microsoft.graph.conflictBehavior': 'rename'
|
|
845
|
+
}),
|
|
846
|
+
headers: {
|
|
847
|
+
'Content-Type': 'application/json'
|
|
848
|
+
}
|
|
849
|
+
})
|
|
850
|
+
);
|
|
851
|
+
});
|
|
852
|
+
});
|