@softeria/ms-365-mcp-server 0.3.3 → 0.3.5

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
@@ -17,6 +17,11 @@ A Model Context Protocol (MCP) server for interacting with Microsoft 365 service
17
17
  - Calendar event management
18
18
  - Mail operations
19
19
  - OneDrive file management
20
+ - OneNote notebooks and pages
21
+ - To Do tasks and task lists
22
+ - Planner plans and tasks
23
+ - Outlook contacts
24
+ - User management
20
25
  - Dynamic tools powered by Microsoft Graph OpenAPI spec
21
26
  - Built on the Model Context Protocol
22
27
 
@@ -24,7 +29,7 @@ A Model Context Protocol (MCP) server for interacting with Microsoft 365 service
24
29
 
25
30
  Test login in Claude Desktop:
26
31
 
27
- ![Login example](![Image](https://github.com/user-attachments/assets/e457884f-c98a-4186-9e6f-eb323ec24e0a)
32
+ ![Login example](https://github.com/user-attachments/assets/e457884f-c98a-4186-9e6f-eb323ec24e0a)
28
33
 
29
34
  ## Examples
30
35
 
@@ -69,23 +74,14 @@ integration method.
69
74
  - Call the `login` tool (auto-checks existing token)
70
75
  - If needed, get URL+code, visit in browser
71
76
  - Use `verify-login` tool to confirm
72
- -
73
77
  2. **Optional CLI login**:
74
78
  ```bash
75
79
  npx @softeria/ms-365-mcp-server --login
76
80
  ```
77
- Follow the URL and code prompt in terminal.
81
+ Follow the URL and code prompt in the terminal.
78
82
 
79
83
  Tokens are cached securely in your OS credential store (fallback to file).
80
84
 
81
- ## Tools
82
-
83
- - **Authentication:** `login`, `logout`, `verify-login`
84
- - **Excel:** list worksheets, get/set ranges, format, sort, chart
85
- - **Calendar:** list/create/update/delete events
86
- - **Mail:** send, read, delete messages
87
- - **OneDrive:** upload, download, list files
88
-
89
85
  ## License
90
86
 
91
87
  MIT © 2025 Softeria
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Microsoft 365 MCP Server",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
package/src/auth.mjs CHANGED
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
4
4
  import path from 'path';
5
5
  import fs from 'fs';
6
6
  import logger from './logger.mjs';
7
+ import { TARGET_ENDPOINTS } from './dynamic-tools.mjs';
7
8
 
8
9
  const SERVICE_NAME = 'ms-365-mcp-server';
9
10
  const TOKEN_CACHE_ACCOUNT = 'msal-token-cache';
@@ -17,17 +18,36 @@ const DEFAULT_CONFIG = {
17
18
  },
18
19
  };
19
20
 
20
- const DEFAULT_SCOPES = [
21
- 'Files.ReadWrite',
22
- 'User.Read',
23
- 'Calendars.Read',
24
- 'Calendars.ReadWrite',
25
- 'Mail.Read',
26
- 'Mail.ReadWrite',
27
- ];
21
+ const SCOPE_HIERARCHY = {
22
+ 'Mail.ReadWrite': ['Mail.Read', 'Mail.Send'],
23
+ 'Calendars.ReadWrite': ['Calendars.Read'],
24
+ 'Files.ReadWrite': ['Files.Read'],
25
+ 'Tasks.ReadWrite': ['Tasks.Read'],
26
+ 'Contacts.ReadWrite': ['Contacts.Read'],
27
+ };
28
+
29
+ function buildScopesFromEndpoints() {
30
+ const scopesSet = new Set();
31
+
32
+ TARGET_ENDPOINTS.forEach((endpoint) => {
33
+ if (endpoint.scopes && Array.isArray(endpoint.scopes)) {
34
+ endpoint.scopes.forEach((scope) => scopesSet.add(scope));
35
+ }
36
+ });
37
+
38
+ Object.entries(SCOPE_HIERARCHY).forEach(([higherScope, lowerScopes]) => {
39
+ if (lowerScopes.every((scope) => scopesSet.has(scope))) {
40
+ lowerScopes.forEach((scope) => scopesSet.delete(scope));
41
+ scopesSet.add(higherScope);
42
+ }
43
+ });
44
+
45
+ return Array.from(scopesSet);
46
+ }
28
47
 
29
48
  class AuthManager {
30
- constructor(config = DEFAULT_CONFIG, scopes = DEFAULT_SCOPES) {
49
+ constructor(config = DEFAULT_CONFIG, scopes = buildScopesFromEndpoints()) {
50
+ logger.info(`And scopes are ${scopes.join(', ')}`, scopes);
31
51
  this.config = config;
32
52
  this.scopes = scopes;
33
53
  this.msalApp = new PublicClientApplication(this.config);
@@ -6,136 +6,337 @@ import {
6
6
  isMethodWithBody,
7
7
  loadOpenApiSpec,
8
8
  } from './openapi-helpers.mjs';
9
+ import { z } from 'zod';
10
+
11
+ /**
12
+ * Validates all endpoints in TARGET_ENDPOINTS against the OpenAPI spec.
13
+ * Returns an array of endpoints that don't exist in the spec.
14
+ *
15
+ * @returns {Array} Array of missing endpoints
16
+ */
17
+ export function validateEndpoints() {
18
+ const openapi = loadOpenApiSpec();
19
+ const missingEndpoints = [];
20
+
21
+ for (const endpoint of TARGET_ENDPOINTS) {
22
+ const result = findPathAndOperation(openapi, endpoint.pathPattern, endpoint.method);
23
+ if (!result) {
24
+ missingEndpoints.push({
25
+ toolName: endpoint.toolName,
26
+ pathPattern: endpoint.pathPattern,
27
+ method: endpoint.method,
28
+ });
29
+ }
30
+ }
31
+
32
+ return missingEndpoints;
33
+ }
9
34
 
10
35
  export const TARGET_ENDPOINTS = [
11
36
  {
12
37
  pathPattern: '/me/messages',
13
38
  method: 'get',
14
39
  toolName: 'list-mail-messages',
40
+ scopes: ['Mail.Read'],
15
41
  },
16
42
  {
17
43
  pathPattern: '/me/mailFolders',
18
44
  method: 'get',
19
45
  toolName: 'list-mail-folders',
46
+ scopes: ['Mail.Read'],
20
47
  },
21
48
  {
22
49
  pathPattern: '/me/mailFolders/{mailFolder-id}/messages',
23
50
  method: 'get',
24
51
  toolName: 'list-mail-folder-messages',
52
+ scopes: ['Mail.Read'],
25
53
  },
26
54
  {
27
55
  pathPattern: '/me/messages/{message-id}',
28
56
  method: 'get',
29
57
  toolName: 'get-mail-message',
58
+ scopes: ['Mail.Read'],
30
59
  },
60
+ {
61
+ pathPattern: '/me/messages',
62
+ method: 'post',
63
+ toolName: 'send-mail',
64
+ scopes: ['Mail.Send'],
65
+ },
66
+ {
67
+ pathPattern: '/me/messages/{message-id}',
68
+ method: 'delete',
69
+ toolName: 'delete-mail-message',
70
+ scopes: ['Mail.ReadWrite'],
71
+ },
72
+
31
73
  {
32
74
  pathPattern: '/me/events',
33
75
  method: 'get',
34
76
  toolName: 'list-calendar-events',
77
+ scopes: ['Calendars.Read'],
35
78
  },
36
79
  {
37
80
  pathPattern: '/me/events/{event-id}',
38
81
  method: 'get',
39
82
  toolName: 'get-calendar-event',
83
+ scopes: ['Calendars.Read'],
40
84
  },
41
85
  {
42
86
  pathPattern: '/me/events',
43
87
  method: 'post',
44
88
  toolName: 'create-calendar-event',
89
+ scopes: ['Calendars.ReadWrite'],
45
90
  },
46
91
  {
47
92
  pathPattern: '/me/events/{event-id}',
48
93
  method: 'patch',
49
94
  toolName: 'update-calendar-event',
95
+ scopes: ['Calendars.ReadWrite'],
50
96
  },
51
97
  {
52
98
  pathPattern: '/me/events/{event-id}',
53
99
  method: 'delete',
54
100
  toolName: 'delete-calendar-event',
101
+ scopes: ['Calendars.ReadWrite'],
55
102
  },
56
103
  {
57
104
  pathPattern: '/me/calendarView',
58
105
  method: 'get',
59
106
  toolName: 'get-calendar-view',
107
+ scopes: ['Calendars.Read'],
60
108
  },
61
109
  {
62
- pathPattern: '/users/{user-id}/drive',
110
+ pathPattern: '/me/calendars',
63
111
  method: 'get',
64
- toolName: 'get-user-drive',
112
+ toolName: 'list-calendars',
113
+ scopes: ['Calendars.Read'],
65
114
  },
115
+
66
116
  {
67
117
  pathPattern: '/drives',
68
118
  method: 'get',
69
119
  toolName: 'list-drives',
120
+ scopes: ['Files.Read'],
70
121
  },
71
122
  {
72
123
  pathPattern: '/drives/{drive-id}/root',
73
124
  method: 'get',
74
125
  toolName: 'get-drive-root-item',
126
+ scopes: ['Files.Read'],
75
127
  },
76
128
  {
77
129
  pathPattern: '/drives/{drive-id}/root',
78
130
  method: 'get',
79
131
  toolName: 'get-root-folder',
132
+ scopes: ['Files.Read'],
80
133
  },
81
134
  {
82
135
  pathPattern: '/drives/{drive-id}/items/{driveItem-id}/children',
83
136
  method: 'get',
84
137
  toolName: 'list-folder-files',
138
+ scopes: ['Files.Read'],
85
139
  },
86
140
  {
87
141
  pathPattern: '/drives/{drive-id}/items/{driveItem-id}/children',
88
142
  method: 'post',
89
143
  toolName: 'create-item-in-folder',
144
+ scopes: ['Files.ReadWrite'],
90
145
  },
91
146
  {
92
147
  pathPattern: '/drives/{drive-id}/items/{driveItem-id}/children/{driveItem-id1}/content',
93
148
  method: 'get',
94
- toolName: 'download-file-content',
149
+ toolName: 'download-onedrive-file-content',
150
+ scopes: ['Files.Read'],
95
151
  },
96
152
  {
97
153
  pathPattern: '/drives/{drive-id}/items/{driveItem-id}',
98
154
  method: 'delete',
99
- toolName: 'delete-file',
155
+ toolName: 'delete-onedrive-file',
156
+ scopes: ['Files.ReadWrite'],
100
157
  },
101
158
  {
102
159
  pathPattern: '/drives/{drive-id}/items/{driveItem-id}',
103
160
  method: 'patch',
104
- toolName: 'update-file-metadata',
161
+ toolName: 'update-onedrive-file-metadata',
162
+ scopes: ['Files.ReadWrite'],
105
163
  },
164
+
106
165
  {
107
166
  pathPattern:
108
167
  '/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/charts/add',
109
168
  method: 'post',
110
- toolName: 'create-chart',
169
+ toolName: 'create-excel-chart',
111
170
  isExcelOp: true,
171
+ scopes: ['Files.ReadWrite'],
112
172
  },
113
173
  {
114
174
  pathPattern:
115
175
  '/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/range()/format',
116
176
  method: 'patch',
117
- toolName: 'format-range',
177
+ toolName: 'format-excel-range',
118
178
  isExcelOp: true,
179
+ scopes: ['Files.ReadWrite'],
119
180
  },
120
181
  {
121
182
  pathPattern:
122
183
  '/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/range()/sort',
123
184
  method: 'patch',
124
- toolName: 'sort-range',
185
+ toolName: 'sort-excel-range',
125
186
  isExcelOp: true,
187
+ scopes: ['Files.ReadWrite'],
126
188
  },
127
189
  {
128
190
  pathPattern:
129
191
  "/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/range(address='{address}')",
130
192
  method: 'get',
131
- toolName: 'get-range',
193
+ toolName: 'get-excel-range',
132
194
  isExcelOp: true,
195
+ scopes: ['Files.Read'],
133
196
  },
134
197
  {
135
198
  pathPattern: '/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets',
136
199
  method: 'get',
137
- toolName: 'list-worksheets',
200
+ toolName: 'list-excel-worksheets',
138
201
  isExcelOp: true,
202
+ scopes: ['Files.Read'],
203
+ },
204
+
205
+ {
206
+ pathPattern: '/me/onenote/notebooks',
207
+ method: 'get',
208
+ toolName: 'list-onenote-notebooks',
209
+ scopes: ['Notes.Read'],
210
+ },
211
+ {
212
+ pathPattern: '/me/onenote/notebooks/{notebook-id}/sections',
213
+ method: 'get',
214
+ toolName: 'list-onenote-notebook-sections',
215
+ scopes: ['Notes.Read'],
216
+ },
217
+ {
218
+ pathPattern: '/me/onenote/notebooks/{notebook-id}/sections/{onenoteSection-id}/pages',
219
+ method: 'get',
220
+ toolName: 'list-onenote-section-pages',
221
+ scopes: ['Notes.Read'],
222
+ },
223
+ {
224
+ pathPattern: '/me/onenote/pages/{onenotePage-id}/content',
225
+ method: 'get',
226
+ toolName: 'get-onenote-page-content',
227
+ scopes: ['Notes.Read'],
228
+ },
229
+ {
230
+ pathPattern: '/me/onenote/pages',
231
+ method: 'post',
232
+ toolName: 'create-onenote-page',
233
+ scopes: ['Notes.Create'],
234
+ },
235
+
236
+ {
237
+ pathPattern: '/me/todo/lists',
238
+ method: 'get',
239
+ toolName: 'list-todo-task-lists',
240
+ scopes: ['Tasks.Read'],
241
+ },
242
+ {
243
+ pathPattern: '/me/todo/lists/{todoTaskList-id}/tasks',
244
+ method: 'get',
245
+ toolName: 'list-todo-tasks',
246
+ scopes: ['Tasks.Read'],
247
+ },
248
+ {
249
+ pathPattern: '/me/todo/lists/{todoTaskList-id}/tasks/{todoTask-id}',
250
+ method: 'get',
251
+ toolName: 'get-todo-task',
252
+ scopes: ['Tasks.Read'],
253
+ },
254
+ {
255
+ pathPattern: '/me/todo/lists/{todoTaskList-id}/tasks',
256
+ method: 'post',
257
+ toolName: 'create-todo-task',
258
+ scopes: ['Tasks.ReadWrite'],
259
+ },
260
+ {
261
+ pathPattern: '/me/todo/lists/{todoTaskList-id}/tasks/{todoTask-id}',
262
+ method: 'patch',
263
+ toolName: 'update-todo-task',
264
+ scopes: ['Tasks.ReadWrite'],
265
+ },
266
+ {
267
+ pathPattern: '/me/todo/lists/{todoTaskList-id}/tasks/{todoTask-id}',
268
+ method: 'delete',
269
+ toolName: 'delete-todo-task',
270
+ scopes: ['Tasks.ReadWrite'],
271
+ },
272
+
273
+ {
274
+ pathPattern: '/me/planner/tasks',
275
+ method: 'get',
276
+ toolName: 'list-planner-tasks',
277
+ scopes: ['Tasks.Read'],
278
+ },
279
+ {
280
+ pathPattern: '/planner/plans/{plannerPlan-id}',
281
+ method: 'get',
282
+ toolName: 'get-planner-plan',
283
+ scopes: ['Tasks.Read'],
284
+ },
285
+ {
286
+ pathPattern: '/planner/plans/{plannerPlan-id}/tasks',
287
+ method: 'get',
288
+ toolName: 'list-plan-tasks',
289
+ scopes: ['Tasks.Read'],
290
+ },
291
+ {
292
+ pathPattern: '/planner/tasks/{plannerTask-id}',
293
+ method: 'get',
294
+ toolName: 'get-planner-task',
295
+ scopes: ['Tasks.Read'],
296
+ },
297
+ {
298
+ pathPattern: '/planner/tasks',
299
+ method: 'post',
300
+ toolName: 'create-planner-task',
301
+ scopes: ['Tasks.ReadWrite'],
302
+ },
303
+
304
+ {
305
+ pathPattern: '/me/contacts',
306
+ method: 'get',
307
+ toolName: 'list-outlook-contacts',
308
+ scopes: ['Contacts.Read'],
309
+ },
310
+ {
311
+ pathPattern: '/me/contacts/{contact-id}',
312
+ method: 'get',
313
+ toolName: 'get-outlook-contact',
314
+ scopes: ['Contacts.Read'],
315
+ },
316
+ {
317
+ pathPattern: '/me/contacts',
318
+ method: 'post',
319
+ toolName: 'create-outlook-contact',
320
+ scopes: ['Contacts.ReadWrite'],
321
+ },
322
+ {
323
+ pathPattern: '/me/contacts/{contact-id}',
324
+ method: 'patch',
325
+ toolName: 'update-outlook-contact',
326
+ scopes: ['Contacts.ReadWrite'],
327
+ },
328
+ {
329
+ pathPattern: '/me/contacts/{contact-id}',
330
+ method: 'delete',
331
+ toolName: 'delete-outlook-contact',
332
+ scopes: ['Contacts.ReadWrite'],
333
+ },
334
+
335
+ {
336
+ pathPattern: '/me',
337
+ method: 'get',
338
+ toolName: 'get-current-user',
339
+ scopes: ['User.Read'],
139
340
  },
140
341
  ];
141
342
 
@@ -144,6 +345,16 @@ export async function registerDynamicTools(server, graphClient) {
144
345
  const openapi = loadOpenApiSpec();
145
346
  logger.info('Generating dynamic tools from OpenAPI spec...');
146
347
 
348
+ const missingEndpoints = validateEndpoints();
349
+ if (missingEndpoints.length > 0) {
350
+ logger.warn('Some endpoints are missing from the OpenAPI spec:');
351
+ missingEndpoints.forEach((endpoint) => {
352
+ logger.warn(
353
+ `- Tool: ${endpoint.toolName}, Path: ${endpoint.pathPattern}, Method: ${endpoint.method}`
354
+ );
355
+ });
356
+ }
357
+
147
358
  for (const endpoint of TARGET_ENDPOINTS) {
148
359
  const result = findPathAndOperation(openapi, endpoint.pathPattern, endpoint.method);
149
360
  if (!result) continue;
@@ -157,13 +368,13 @@ export async function registerDynamicTools(server, graphClient) {
157
368
  const paramsSchema = buildParameterSchemas(endpoint, operation);
158
369
 
159
370
  if (endpoint.hasCustomParams) {
160
- if (endpoint.toolName === 'upload-file') {
371
+ if (endpoint.toolName === 'upload-onedrive-file') {
161
372
  paramsSchema.content = z.string().describe('File content to upload');
162
373
  paramsSchema.contentType = z
163
374
  .string()
164
375
  .optional()
165
376
  .describe('Content type of the file (e.g., "application/pdf", "image/jpeg")');
166
- } else if (endpoint.toolName === 'create-folder') {
377
+ } else if (endpoint.toolName === 'create-onedrive-folder') {
167
378
  paramsSchema.name = z.string().describe('Name of the folder to create');
168
379
  paramsSchema.description = z.string().optional().describe('Description of the folder');
169
380
  }
@@ -193,13 +404,13 @@ export async function registerDynamicTools(server, graphClient) {
193
404
  options.excelFile = params.filePath;
194
405
  }
195
406
 
196
- if (endpoint.toolName === 'download-file') {
407
+ if (endpoint.toolName === 'download-onedrive-file-content') {
197
408
  options.rawResponse = true;
198
409
  }
199
410
 
200
411
  const url = buildRequestUrl(endpoint.pathPattern, params, pathParams, operation.parameters);
201
412
 
202
- if (endpoint.toolName === 'upload-file' && params.content) {
413
+ if (endpoint.toolName === 'upload-onedrive-file' && params.content) {
203
414
  options.body = params.content;
204
415
  options.headers = {
205
416
  'Content-Type': params.contentType || 'application/octet-stream',
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { validateEndpoints } from '../src/dynamic-tools.mjs';
3
+
4
+ /**
5
+ * This test file ensures that all the mappings in TARGET_ENDPOINTS actually match
6
+ * the endpoints in the OpenAPI spec. It helps catch issues where:
7
+ *
8
+ * 1. An endpoint in TARGET_ENDPOINTS doesn't exist in the OpenAPI spec
9
+ * 2. The method for an endpoint doesn't match what's in the OpenAPI spec
10
+ *
11
+ * This is a more automated approach than manually running the app and tailing logs.
12
+ */
13
+
14
+ describe('Mappings Validation', () => {
15
+ it('should verify all TARGET_ENDPOINTS exist in the OpenAPI spec', () => {
16
+ const missingEndpoints = validateEndpoints();
17
+
18
+ if (missingEndpoints.length > 0) {
19
+ console.error('The following endpoints are missing from the OpenAPI spec:');
20
+ missingEndpoints.forEach((endpoint) => {
21
+ console.error(
22
+ `- Tool: ${endpoint.toolName}, Path: ${endpoint.pathPattern}, Method: ${endpoint.method}`
23
+ );
24
+ });
25
+ }
26
+
27
+ expect(missingEndpoints).toEqual([]);
28
+ });
29
+ });