@softeria/ms-365-mcp-server 0.1.11 → 0.2.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.
@@ -0,0 +1,554 @@
1
+ import { z } from 'zod';
2
+
3
+ export function registerFilesTools(server, graphClient) {
4
+ server.tool(
5
+ 'list-drives',
6
+ {
7
+ userId: z
8
+ .string()
9
+ .optional()
10
+ .describe(
11
+ "ID of the user whose drives to list. If not specified, the current user's drives will be listed."
12
+ ),
13
+ },
14
+ async ({ userId }) => {
15
+ const endpoint = userId ? `/users/${userId}/drives` : '/me/drives';
16
+
17
+ return graphClient.graphRequest(endpoint, {
18
+ method: 'GET',
19
+ });
20
+ }
21
+ );
22
+
23
+ server.tool(
24
+ 'list-files',
25
+ {
26
+ path: z
27
+ .string()
28
+ .optional()
29
+ .describe('Path to list files from. Use "/" for root. Include leading slash.'),
30
+ itemId: z
31
+ .string()
32
+ .optional()
33
+ .describe(
34
+ 'ID of the specific item to list children from. Use this instead of path for direct item access.'
35
+ ),
36
+ driveId: z
37
+ .string()
38
+ .optional()
39
+ .describe('ID of the drive to use. If not specified, the default drive will be used.'),
40
+ userId: z
41
+ .string()
42
+ .optional()
43
+ .describe(
44
+ "ID of the user whose drive to access. If not specified, the current user's drive will be used."
45
+ ),
46
+ expand: z
47
+ .string()
48
+ .optional()
49
+ .describe('Comma-separated list of relationships to expand in the response.'),
50
+ select: z
51
+ .string()
52
+ .optional()
53
+ .describe('Comma-separated list of properties to include in the response.'),
54
+ top: z
55
+ .number()
56
+ .optional()
57
+ .describe('Number of items to return in a result. Default and maximum value is 999.'),
58
+ },
59
+ async ({ path, itemId, driveId, userId, expand, select, top }) => {
60
+ let endpoint;
61
+
62
+ if (driveId) {
63
+ if (itemId) {
64
+ endpoint = `/drives/${driveId}/items/${itemId}/children`;
65
+ } else if (path === '/' || !path) {
66
+ endpoint = `/drives/${driveId}/root/children`;
67
+ } else {
68
+ endpoint = `/drives/${driveId}/root:${path}:/children`;
69
+ }
70
+ } else if (userId && userId !== 'me') {
71
+ if (itemId) {
72
+ endpoint = `/users/${userId}/drive/items/${itemId}/children`;
73
+ } else if (path === '/' || !path) {
74
+ endpoint = `/users/${userId}/drive/root/children`;
75
+ } else {
76
+ endpoint = `/users/${userId}/drive/root:${path}:/children`;
77
+ }
78
+ } else {
79
+ if (itemId) {
80
+ endpoint = `/me/drive/items/${itemId}/children`;
81
+ } else if (path === '/' || !path) {
82
+ endpoint = `/me/drive/root/children`;
83
+ } else {
84
+ endpoint = `/me/drive/root:${path}:/children`;
85
+ }
86
+ }
87
+
88
+ const queryParams = new URLSearchParams();
89
+
90
+ if (expand) {
91
+ queryParams.append('$expand', expand);
92
+ }
93
+
94
+ if (select) {
95
+ queryParams.append('$select', select);
96
+ }
97
+
98
+ if (top) {
99
+ queryParams.append('$top', top.toString());
100
+ }
101
+
102
+ const queryString = queryParams.toString();
103
+ if (queryString) {
104
+ endpoint += `?${queryString}`;
105
+ }
106
+
107
+ return graphClient.graphRequest(endpoint, {
108
+ method: 'GET',
109
+ });
110
+ }
111
+ );
112
+
113
+ server.tool(
114
+ 'get-file',
115
+ {
116
+ path: z
117
+ .string()
118
+ .optional()
119
+ .describe('Path to the file, including leading slash. e.g. "/Documents/report.docx"'),
120
+ itemId: z
121
+ .string()
122
+ .optional()
123
+ .describe(
124
+ 'ID of the specific item to retrieve. Use this instead of path for direct item access.'
125
+ ),
126
+ driveId: z
127
+ .string()
128
+ .optional()
129
+ .describe('ID of the drive to use. If not specified, the default drive will be used.'),
130
+ userId: z
131
+ .string()
132
+ .optional()
133
+ .describe(
134
+ "ID of the user whose drive to access. If not specified, the current user's drive will be used."
135
+ ),
136
+ expand: z
137
+ .string()
138
+ .optional()
139
+ .describe('Comma-separated list of relationships to expand in the response.'),
140
+ select: z
141
+ .string()
142
+ .optional()
143
+ .describe('Comma-separated list of properties to include in the response.'),
144
+ includeDeletedItems: z
145
+ .boolean()
146
+ .optional()
147
+ .describe(
148
+ 'For OneDrive Personal, specifies whether to include deleted items in the response.'
149
+ ),
150
+ },
151
+ async ({ path, itemId, driveId, userId, expand, select, includeDeletedItems }) => {
152
+ if (!path && !itemId) {
153
+ throw new Error('Either path or itemId must be provided');
154
+ }
155
+
156
+ let endpoint;
157
+
158
+ if (driveId) {
159
+ if (itemId) {
160
+ endpoint = `/drives/${driveId}/items/${itemId}`;
161
+ } else {
162
+ endpoint = `/drives/${driveId}/root:${path}`;
163
+ }
164
+ } else if (userId && userId !== 'me') {
165
+ if (itemId) {
166
+ endpoint = `/users/${userId}/drive/items/${itemId}`;
167
+ } else {
168
+ endpoint = `/users/${userId}/drive/root:${path}`;
169
+ }
170
+ } else {
171
+ if (itemId) {
172
+ endpoint = `/me/drive/items/${itemId}`;
173
+ } else {
174
+ endpoint = `/me/drive/root:${path}`;
175
+ }
176
+ }
177
+
178
+ const queryParams = new URLSearchParams();
179
+
180
+ if (expand) {
181
+ queryParams.append('$expand', expand);
182
+ }
183
+
184
+ if (select) {
185
+ queryParams.append('$select', select);
186
+ }
187
+
188
+ if (includeDeletedItems) {
189
+ queryParams.append('includeDeletedItems', 'true');
190
+ }
191
+
192
+ const queryString = queryParams.toString();
193
+ if (queryString) {
194
+ endpoint += `?${queryString}`;
195
+ }
196
+
197
+ return graphClient.graphRequest(endpoint, {
198
+ method: 'GET',
199
+ });
200
+ }
201
+ );
202
+
203
+ server.tool(
204
+ 'create-folder',
205
+ {
206
+ parentPath: z
207
+ .string()
208
+ .default('/')
209
+ .describe('Parent folder path. Use "/" for root. Include leading slash.'),
210
+ folderName: z.string().describe('Name of the folder to create'),
211
+ driveId: z
212
+ .string()
213
+ .optional()
214
+ .describe('ID of the drive to use. If not specified, the default drive will be used.'),
215
+ },
216
+ async ({ parentPath, folderName, driveId }) => {
217
+ const folder = {
218
+ name: folderName,
219
+ folder: {},
220
+ '@microsoft.graph.conflictBehavior': 'fail',
221
+ };
222
+
223
+ const endpoint = driveId
224
+ ? `/drives/${driveId}/root:${parentPath}:/children`
225
+ : `/me/drive/root:${parentPath}:/children`;
226
+
227
+ return graphClient.graphRequest(endpoint, {
228
+ method: 'POST',
229
+ body: JSON.stringify(folder),
230
+ });
231
+ }
232
+ );
233
+
234
+ server.tool(
235
+ 'delete-item',
236
+ {
237
+ path: z.string().describe('Path to the file or folder to delete, including leading slash'),
238
+ driveId: z
239
+ .string()
240
+ .optional()
241
+ .describe('ID of the drive to use. If not specified, the default drive will be used.'),
242
+ },
243
+ async ({ path, driveId }) => {
244
+ const endpoint = driveId ? `/drives/${driveId}/root:${path}` : `/me/drive/root:${path}`;
245
+
246
+ return graphClient.graphRequest(endpoint, {
247
+ method: 'DELETE',
248
+ });
249
+ }
250
+ );
251
+
252
+ server.tool(
253
+ 'copy-item',
254
+ {
255
+ sourcePath: z.string().describe('Path to the source file or folder, including leading slash'),
256
+ destinationPath: z
257
+ .string()
258
+ .describe('Path to the destination parent folder, including leading slash'),
259
+ newName: z.string().optional().describe('New name for the item (optional)'),
260
+ sourceDriveId: z
261
+ .string()
262
+ .optional()
263
+ .describe('ID of the source drive. If not specified, the default drive will be used.'),
264
+ destinationDriveId: z
265
+ .string()
266
+ .optional()
267
+ .describe('ID of the destination drive. If not specified, the default drive will be used.'),
268
+ },
269
+ async ({ sourcePath, destinationPath, newName, sourceDriveId, destinationDriveId }) => {
270
+ const copyRequest = {
271
+ parentReference: {
272
+ path: destinationDriveId
273
+ ? `/drives/${destinationDriveId}/root:${destinationPath}`
274
+ : `/drive/root:${destinationPath}`,
275
+ },
276
+ };
277
+
278
+ if (newName) {
279
+ copyRequest.name = newName;
280
+ }
281
+
282
+ const endpoint = sourceDriveId
283
+ ? `/drives/${sourceDriveId}/root:${sourcePath}:/copy`
284
+ : `/me/drive/root:${sourcePath}:/copy`;
285
+
286
+ return graphClient.graphRequest(endpoint, {
287
+ method: 'POST',
288
+ body: JSON.stringify(copyRequest),
289
+ });
290
+ }
291
+ );
292
+
293
+ server.tool(
294
+ 'move-item',
295
+ {
296
+ sourcePath: z.string().describe('Path to the source file or folder, including leading slash'),
297
+ destinationPath: z
298
+ .string()
299
+ .describe('Path to the destination parent folder, including leading slash'),
300
+ newName: z.string().optional().describe('New name for the item (optional)'),
301
+ sourceDriveId: z
302
+ .string()
303
+ .optional()
304
+ .describe('ID of the source drive. If not specified, the default drive will be used.'),
305
+ destinationDriveId: z
306
+ .string()
307
+ .optional()
308
+ .describe('ID of the destination drive. If not specified, the default drive will be used.'),
309
+ },
310
+ async ({ sourcePath, destinationPath, newName, sourceDriveId, destinationDriveId }) => {
311
+ const moveRequest = {
312
+ parentReference: {
313
+ path: destinationDriveId
314
+ ? `/drives/${destinationDriveId}/root:${destinationPath}`
315
+ : `/drive/root:${destinationPath}`,
316
+ },
317
+ };
318
+
319
+ if (newName) {
320
+ moveRequest.name = newName;
321
+ }
322
+
323
+ const endpoint = sourceDriveId
324
+ ? `/drives/${sourceDriveId}/root:${sourcePath}`
325
+ : `/me/drive/root:${sourcePath}`;
326
+
327
+ return graphClient.graphRequest(endpoint, {
328
+ method: 'PATCH',
329
+ body: JSON.stringify(moveRequest),
330
+ });
331
+ }
332
+ );
333
+
334
+ server.tool(
335
+ 'rename-item',
336
+ {
337
+ path: z.string().describe('Path to the file or folder, including leading slash'),
338
+ newName: z.string().describe('New name for the item'),
339
+ driveId: z
340
+ .string()
341
+ .optional()
342
+ .describe('ID of the drive to use. If not specified, the default drive will be used.'),
343
+ },
344
+ async ({ path, newName, driveId }) => {
345
+ const endpoint = driveId ? `/drives/${driveId}/root:${path}` : `/me/drive/root:${path}`;
346
+
347
+ return graphClient.graphRequest(endpoint, {
348
+ method: 'PATCH',
349
+ body: JSON.stringify({ name: newName }),
350
+ });
351
+ }
352
+ );
353
+
354
+ server.tool(
355
+ 'search-files',
356
+ {
357
+ query: z.string().describe('Search query text'),
358
+ folderPath: z
359
+ .string()
360
+ .optional()
361
+ .describe('Optional folder path to limit search scope. Include leading slash.'),
362
+ driveId: z
363
+ .string()
364
+ .optional()
365
+ .describe('ID of the drive to use. If not specified, the default drive will be used.'),
366
+ userId: z
367
+ .string()
368
+ .optional()
369
+ .describe(
370
+ "ID of the user whose drive to search. If not specified, the current user's drive will be used."
371
+ ),
372
+ groupId: z.string().optional().describe('ID of the group whose drive to search.'),
373
+ siteId: z.string().optional().describe('ID of the SharePoint site whose drive to search.'),
374
+ expand: z
375
+ .string()
376
+ .optional()
377
+ .describe('Comma-separated list of relationships to expand in the response.'),
378
+ select: z
379
+ .string()
380
+ .optional()
381
+ .describe('Comma-separated list of properties to include in the response.'),
382
+ top: z.number().optional().describe('Number of items to return in a result set.'),
383
+ orderby: z
384
+ .string()
385
+ .optional()
386
+ .describe('Comma-separated list of properties for sorting results.'),
387
+ skipToken: z
388
+ .string()
389
+ .optional()
390
+ .describe('Paging token from a previous request to continue listing results.'),
391
+ },
392
+ async ({
393
+ query,
394
+ folderPath,
395
+ driveId,
396
+ userId,
397
+ groupId,
398
+ siteId,
399
+ expand,
400
+ select,
401
+ top,
402
+ orderby,
403
+ skipToken,
404
+ }) => {
405
+ const contexts = [driveId, userId, groupId, siteId].filter(Boolean).length;
406
+ if (contexts > 1) {
407
+ throw new Error('Only one of driveId, userId, groupId, or siteId can be specified');
408
+ }
409
+
410
+ let endpoint;
411
+
412
+ if (driveId) {
413
+ endpoint = folderPath
414
+ ? `/drives/${driveId}/root:${folderPath}:/search(q='`
415
+ : `/drives/${driveId}/root/search(q='`;
416
+ } else if (groupId) {
417
+ endpoint = folderPath
418
+ ? `/groups/${groupId}/drive/root:${folderPath}:/search(q='`
419
+ : `/groups/${groupId}/drive/root/search(q='`;
420
+ } else if (siteId) {
421
+ endpoint = folderPath
422
+ ? `/sites/${siteId}/drive/root:${folderPath}:/search(q='`
423
+ : `/sites/${siteId}/drive/root/search(q='`;
424
+ } else if (userId && userId !== 'me') {
425
+ endpoint = folderPath
426
+ ? `/users/${userId}/drive/root:${folderPath}:/search(q='`
427
+ : `/users/${userId}/drive/root/search(q='`;
428
+ } else {
429
+ endpoint = folderPath
430
+ ? `/me/drive/root:${folderPath}:/search(q='`
431
+ : `/me/drive/root/search(q='`;
432
+ }
433
+
434
+ endpoint += encodeURIComponent(query) + "'";
435
+ endpoint += ')';
436
+
437
+ const queryParams = new URLSearchParams();
438
+
439
+ if (expand) {
440
+ queryParams.append('$expand', expand);
441
+ }
442
+
443
+ if (select) {
444
+ queryParams.append('$select', select);
445
+ }
446
+
447
+ if (top) {
448
+ queryParams.append('$top', top.toString());
449
+ }
450
+
451
+ if (orderby) {
452
+ queryParams.append('$orderby', orderby);
453
+ }
454
+
455
+ if (skipToken) {
456
+ queryParams.append('$skipToken', skipToken);
457
+ }
458
+
459
+ const queryString = queryParams.toString();
460
+ if (queryString) {
461
+ endpoint += `?${queryString}`;
462
+ }
463
+
464
+ return graphClient.graphRequest(endpoint, {
465
+ method: 'GET',
466
+ });
467
+ }
468
+ );
469
+
470
+ server.tool(
471
+ 'get-shared-items',
472
+ {
473
+ driveId: z
474
+ .string()
475
+ .optional()
476
+ .describe('ID of the drive to use. If not specified, the default drive will be used.'),
477
+ },
478
+ async ({ driveId }) => {
479
+ const endpoint = driveId ? `/drives/${driveId}/sharedWithMe` : `/me/drive/sharedWithMe`;
480
+
481
+ return graphClient.graphRequest(endpoint, {
482
+ method: 'GET',
483
+ });
484
+ }
485
+ );
486
+
487
+ server.tool(
488
+ 'create-sharing-link',
489
+ {
490
+ path: z.string().describe('Path to the file or folder, including leading slash'),
491
+ type: z
492
+ .enum(['view', 'edit', 'embed'])
493
+ .default('view')
494
+ .describe('Type of sharing link to create'),
495
+ scope: z
496
+ .enum(['anonymous', 'organization'])
497
+ .default('anonymous')
498
+ .describe('Scope of the sharing link'),
499
+ password: z.string().optional().describe('Password for the sharing link (optional)'),
500
+ expirationDateTime: z
501
+ .string()
502
+ .optional()
503
+ .describe('Expiration date and time (ISO format, optional)'),
504
+ driveId: z
505
+ .string()
506
+ .optional()
507
+ .describe('ID of the drive to use. If not specified, the default drive will be used.'),
508
+ },
509
+ async ({ path, type, scope, password, expirationDateTime, driveId }) => {
510
+ const permissions = {
511
+ type: type,
512
+ scope: scope,
513
+ };
514
+
515
+ if (password) {
516
+ permissions.password = password;
517
+ }
518
+
519
+ if (expirationDateTime) {
520
+ permissions.expirationDateTime = expirationDateTime;
521
+ }
522
+
523
+ const endpoint = driveId
524
+ ? `/drives/${driveId}/root:${path}:/createLink`
525
+ : `/me/drive/root:${path}:/createLink`;
526
+
527
+ return graphClient.graphRequest(endpoint, {
528
+ method: 'POST',
529
+ body: JSON.stringify(permissions),
530
+ });
531
+ }
532
+ );
533
+
534
+ server.tool(
535
+ 'get-file-content',
536
+ {
537
+ path: z.string().describe('Path to the file, including leading slash'),
538
+ driveId: z
539
+ .string()
540
+ .optional()
541
+ .describe('ID of the drive to use. If not specified, the default drive will be used.'),
542
+ },
543
+ async ({ path, driveId }) => {
544
+ const endpoint = driveId
545
+ ? `/drives/${driveId}/root:${path}:/content`
546
+ : `/me/drive/root:${path}:/content`;
547
+
548
+ return graphClient.graphRequest(endpoint, {
549
+ method: 'GET',
550
+ rawResponse: true,
551
+ });
552
+ }
553
+ );
554
+ }