@softeria/ms-365-mcp-server 0.3.5 → 0.4.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.
Files changed (49) hide show
  1. package/.github/workflows/build.yml +3 -0
  2. package/.github/workflows/npm-publish.yml +2 -0
  3. package/README.md +1 -1
  4. package/bin/generate-graph-client.mjs +59 -0
  5. package/bin/{download-openapi.mjs → modules/download-openapi.mjs} +10 -20
  6. package/bin/modules/extract-descriptions.mjs +48 -0
  7. package/bin/modules/generate-mcp-tools.mjs +36 -0
  8. package/bin/modules/simplified-openapi.mjs +78 -0
  9. package/dist/auth-tools.js +80 -0
  10. package/dist/auth.js +219 -0
  11. package/dist/cli.js +21 -0
  12. package/dist/endpoints.json +375 -0
  13. package/dist/generated/client.js +14683 -0
  14. package/dist/generated/endpoint-types.js +1 -0
  15. package/dist/generated/hack.js +37 -0
  16. package/dist/graph-client.js +254 -0
  17. package/dist/graph-tools.js +98 -0
  18. package/dist/index.js +39 -0
  19. package/dist/logger.js +33 -0
  20. package/dist/server.js +32 -0
  21. package/{src/version.mjs → dist/version.js} +0 -2
  22. package/package.json +12 -9
  23. package/src/{auth-tools.mjs → auth-tools.ts} +7 -5
  24. package/src/{auth.mjs → auth.ts} +60 -30
  25. package/src/{cli.mjs → cli.ts} +9 -1
  26. package/src/endpoints.json +375 -0
  27. package/src/generated/README.md +51 -0
  28. package/src/generated/client.ts +24916 -0
  29. package/src/generated/endpoint-types.ts +27 -0
  30. package/src/generated/hack.ts +50 -0
  31. package/src/{graph-client.mjs → graph-client.ts} +53 -18
  32. package/src/graph-tools.ts +174 -0
  33. package/{index.mjs → src/index.ts} +6 -6
  34. package/src/{logger.mjs → logger.ts} +1 -1
  35. package/src/{server.mjs → server.ts} +16 -9
  36. package/src/version.ts +9 -0
  37. package/test/{auth-tools.test.js → auth-tools.test.ts} +41 -38
  38. package/test/{cli.test.js → cli.test.ts} +3 -3
  39. package/test/{graph-api.test.js → graph-api.test.ts} +5 -5
  40. package/test/test-hack.ts +17 -0
  41. package/tsconfig.json +16 -0
  42. package/src/dynamic-tools.mjs +0 -442
  43. package/src/openapi-helpers.mjs +0 -187
  44. package/src/param-mapper.mjs +0 -30
  45. package/test/dynamic-tools.test.js +0 -852
  46. package/test/mappings.test.js +0 -29
  47. package/test/mcp-server.test.js +0 -36
  48. package/test/openapi-helpers.test.js +0 -210
  49. package/test/param-mapper.test.js +0 -56
@@ -1,24 +1,32 @@
1
1
  import { PublicClientApplication } from '@azure/msal-node';
2
+ import type { Configuration } from '@azure/msal-node';
2
3
  import keytar from 'keytar';
3
4
  import { fileURLToPath } from 'url';
4
5
  import path from 'path';
5
6
  import fs from 'fs';
6
- import logger from './logger.mjs';
7
- import { TARGET_ENDPOINTS } from './dynamic-tools.mjs';
7
+ import logger from './logger.js';
8
+
9
+ const endpoints = await import('./endpoints.json', {
10
+ assert: { type: 'json' },
11
+ });
8
12
 
9
13
  const SERVICE_NAME = 'ms-365-mcp-server';
10
14
  const TOKEN_CACHE_ACCOUNT = 'msal-token-cache';
11
15
  const FALLBACK_DIR = path.dirname(fileURLToPath(import.meta.url));
12
16
  const FALLBACK_PATH = path.join(FALLBACK_DIR, '..', '.token-cache.json');
13
17
 
14
- const DEFAULT_CONFIG = {
18
+ const DEFAULT_CONFIG: Configuration = {
15
19
  auth: {
16
20
  clientId: '084a3e9f-a9f4-43f7-89f9-d229cf97853e',
17
21
  authority: 'https://login.microsoftonline.com/common',
18
22
  },
19
23
  };
20
24
 
21
- const SCOPE_HIERARCHY = {
25
+ interface ScopeHierarchy {
26
+ [key: string]: string[];
27
+ }
28
+
29
+ const SCOPE_HIERARCHY: ScopeHierarchy = {
22
30
  'Mail.ReadWrite': ['Mail.Read', 'Mail.Send'],
23
31
  'Calendars.ReadWrite': ['Calendars.Read'],
24
32
  'Files.ReadWrite': ['Files.Read'],
@@ -26,10 +34,10 @@ const SCOPE_HIERARCHY = {
26
34
  'Contacts.ReadWrite': ['Contacts.Read'],
27
35
  };
28
36
 
29
- function buildScopesFromEndpoints() {
30
- const scopesSet = new Set();
37
+ function buildScopesFromEndpoints(): string[] {
38
+ const scopesSet = new Set<string>();
31
39
 
32
- TARGET_ENDPOINTS.forEach((endpoint) => {
40
+ endpoints.default.forEach((endpoint) => {
33
41
  if (endpoint.scopes && Array.isArray(endpoint.scopes)) {
34
42
  endpoint.scopes.forEach((scope) => scopesSet.add(scope));
35
43
  }
@@ -45,8 +53,26 @@ function buildScopesFromEndpoints() {
45
53
  return Array.from(scopesSet);
46
54
  }
47
55
 
56
+ interface LoginTestResult {
57
+ success: boolean;
58
+ message: string;
59
+ userData?: {
60
+ displayName: string;
61
+ userPrincipalName: string;
62
+ };
63
+ }
64
+
48
65
  class AuthManager {
49
- constructor(config = DEFAULT_CONFIG, scopes = buildScopesFromEndpoints()) {
66
+ private config: Configuration;
67
+ private scopes: string[];
68
+ private msalApp: PublicClientApplication;
69
+ private accessToken: string | null;
70
+ private tokenExpiry: number | null;
71
+
72
+ constructor(
73
+ config: Configuration = DEFAULT_CONFIG,
74
+ scopes: string[] = buildScopesFromEndpoints()
75
+ ) {
50
76
  logger.info(`And scopes are ${scopes.join(', ')}`, scopes);
51
77
  this.config = config;
52
78
  this.scopes = scopes;
@@ -55,9 +81,9 @@ class AuthManager {
55
81
  this.tokenExpiry = null;
56
82
  }
57
83
 
58
- async loadTokenCache() {
84
+ async loadTokenCache(): Promise<void> {
59
85
  try {
60
- let cacheData;
86
+ let cacheData: string | undefined;
61
87
 
62
88
  try {
63
89
  const cachedData = await keytar.getPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
@@ -65,7 +91,9 @@ class AuthManager {
65
91
  cacheData = cachedData;
66
92
  }
67
93
  } catch (keytarError) {
68
- logger.warn(`Keychain access failed, falling back to file storage: ${keytarError.message}`);
94
+ logger.warn(
95
+ `Keychain access failed, falling back to file storage: ${(keytarError as Error).message}`
96
+ );
69
97
  }
70
98
 
71
99
  if (!cacheData && fs.existsSync(FALLBACK_PATH)) {
@@ -76,27 +104,29 @@ class AuthManager {
76
104
  this.msalApp.getTokenCache().deserialize(cacheData);
77
105
  }
78
106
  } catch (error) {
79
- logger.error(`Error loading token cache: ${error.message}`);
107
+ logger.error(`Error loading token cache: ${(error as Error).message}`);
80
108
  }
81
109
  }
82
110
 
83
- async saveTokenCache() {
111
+ async saveTokenCache(): Promise<void> {
84
112
  try {
85
113
  const cacheData = this.msalApp.getTokenCache().serialize();
86
114
 
87
115
  try {
88
116
  await keytar.setPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT, cacheData);
89
117
  } catch (keytarError) {
90
- logger.warn(`Keychain save failed, falling back to file storage: ${keytarError.message}`);
118
+ logger.warn(
119
+ `Keychain save failed, falling back to file storage: ${(keytarError as Error).message}`
120
+ );
91
121
 
92
122
  fs.writeFileSync(FALLBACK_PATH, cacheData);
93
123
  }
94
124
  } catch (error) {
95
- logger.error(`Error saving token cache: ${error.message}`);
125
+ logger.error(`Error saving token cache: ${(error as Error).message}`);
96
126
  }
97
127
  }
98
128
 
99
- async getToken(forceRefresh = false) {
129
+ async getToken(forceRefresh = false): Promise<string | null> {
100
130
  if (this.accessToken && this.tokenExpiry && this.tokenExpiry > Date.now() && !forceRefresh) {
101
131
  return this.accessToken;
102
132
  }
@@ -112,7 +142,7 @@ class AuthManager {
112
142
  try {
113
143
  const response = await this.msalApp.acquireTokenSilent(silentRequest);
114
144
  this.accessToken = response.accessToken;
115
- this.tokenExpiry = new Date(response.expiresOn).getTime();
145
+ this.tokenExpiry = response.expiresOn ? new Date(response.expiresOn).getTime() : null;
116
146
  return this.accessToken;
117
147
  } catch (error) {
118
148
  logger.info('Silent token acquisition failed, using device code flow');
@@ -122,10 +152,10 @@ class AuthManager {
122
152
  throw new Error('No valid token found');
123
153
  }
124
154
 
125
- async acquireTokenByDeviceCode(hack) {
155
+ async acquireTokenByDeviceCode(hack?: (message: string) => void): Promise<string | null> {
126
156
  const deviceCodeRequest = {
127
157
  scopes: this.scopes,
128
- deviceCodeCallback: (response) => {
158
+ deviceCodeCallback: (response: { message: string }) => {
129
159
  const text = ['\n', response.message, '\n'].join('');
130
160
  if (hack) {
131
161
  hack(text + 'After login run the "verify login" command');
@@ -140,17 +170,17 @@ class AuthManager {
140
170
  logger.info('Requesting device code...');
141
171
  const response = await this.msalApp.acquireTokenByDeviceCode(deviceCodeRequest);
142
172
  logger.info('Device code login successful');
143
- this.accessToken = response.accessToken;
144
- this.tokenExpiry = new Date(response.expiresOn).getTime();
173
+ this.accessToken = response?.accessToken || null;
174
+ this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
145
175
  await this.saveTokenCache();
146
176
  return this.accessToken;
147
177
  } catch (error) {
148
- logger.error(`Error in device code flow: ${error.message}`);
178
+ logger.error(`Error in device code flow: ${(error as Error).message}`);
149
179
  throw error;
150
180
  }
151
181
  }
152
182
 
153
- async testLogin() {
183
+ async testLogin(): Promise<LoginTestResult> {
154
184
  try {
155
185
  logger.info('Testing login...');
156
186
  const token = await this.getToken();
@@ -192,22 +222,22 @@ class AuthManager {
192
222
  };
193
223
  }
194
224
  } catch (graphError) {
195
- logger.error(`Error fetching user data: ${graphError.message}`);
225
+ logger.error(`Error fetching user data: ${(graphError as Error).message}`);
196
226
  return {
197
227
  success: false,
198
- message: `Login successful but Graph API access failed: ${graphError.message}`,
228
+ message: `Login successful but Graph API access failed: ${(graphError as Error).message}`,
199
229
  };
200
230
  }
201
231
  } catch (error) {
202
- logger.error(`Login test failed: ${error.message}`);
232
+ logger.error(`Login test failed: ${(error as Error).message}`);
203
233
  return {
204
234
  success: false,
205
- message: `Login failed: ${error.message}`,
235
+ message: `Login failed: ${(error as Error).message}`,
206
236
  };
207
237
  }
208
238
  }
209
239
 
210
- async logout() {
240
+ async logout(): Promise<boolean> {
211
241
  try {
212
242
  const accounts = await this.msalApp.getTokenCache().getAllAccounts();
213
243
  for (const account of accounts) {
@@ -219,7 +249,7 @@ class AuthManager {
219
249
  try {
220
250
  await keytar.deletePassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
221
251
  } catch (keytarError) {
222
- logger.warn(`Keychain deletion failed: ${keytarError.message}`);
252
+ logger.warn(`Keychain deletion failed: ${(keytarError as Error).message}`);
223
253
  }
224
254
 
225
255
  if (fs.existsSync(FALLBACK_PATH)) {
@@ -228,7 +258,7 @@ class AuthManager {
228
258
 
229
259
  return true;
230
260
  } catch (error) {
231
- logger.error(`Error during logout: ${error.message}`);
261
+ logger.error(`Error during logout: ${(error as Error).message}`);
232
262
  throw error;
233
263
  }
234
264
  }
@@ -19,7 +19,15 @@ program
19
19
  .option('--logout', 'Log out and clear saved credentials')
20
20
  .option('--verify-login', 'Verify login without starting the server');
21
21
 
22
- export function parseArgs() {
22
+ export interface CommandOptions {
23
+ v?: boolean;
24
+ login?: boolean;
25
+ logout?: boolean;
26
+ verifyLogin?: boolean;
27
+ [key: string]: any;
28
+ }
29
+
30
+ export function parseArgs(): CommandOptions {
23
31
  program.parse();
24
32
  return program.opts();
25
33
  }
@@ -0,0 +1,375 @@
1
+ [
2
+ {
3
+ "pathPattern": "/me/messages",
4
+ "method": "get",
5
+ "toolName": "list-mail-messages",
6
+ "scopes": [
7
+ "Mail.Read"
8
+ ]
9
+ },
10
+ {
11
+ "pathPattern": "/me/mailFolders",
12
+ "method": "get",
13
+ "toolName": "list-mail-folders",
14
+ "scopes": [
15
+ "Mail.Read"
16
+ ]
17
+ },
18
+ {
19
+ "pathPattern": "/me/mailFolders/{mailFolder-id}/messages",
20
+ "method": "get",
21
+ "toolName": "list-mail-folder-messages",
22
+ "scopes": [
23
+ "Mail.Read"
24
+ ]
25
+ },
26
+ {
27
+ "pathPattern": "/me/messages/{message-id}",
28
+ "method": "get",
29
+ "toolName": "get-mail-message",
30
+ "scopes": [
31
+ "Mail.Read"
32
+ ]
33
+ },
34
+ {
35
+ "pathPattern": "/me/sendMail",
36
+ "method": "post",
37
+ "toolName": "send-mail",
38
+ "scopes": [
39
+ "Mail.Send"
40
+ ]
41
+ },
42
+ {
43
+ "pathPattern": "/me/messages/{message-id}",
44
+ "method": "delete",
45
+ "toolName": "delete-mail-message",
46
+ "scopes": [
47
+ "Mail.ReadWrite"
48
+ ]
49
+ },
50
+ {
51
+ "pathPattern": "/me/events",
52
+ "method": "get",
53
+ "toolName": "list-calendar-events",
54
+ "scopes": [
55
+ "Calendars.Read"
56
+ ]
57
+ },
58
+ {
59
+ "pathPattern": "/me/events/{event-id}",
60
+ "method": "get",
61
+ "toolName": "get-calendar-event",
62
+ "scopes": [
63
+ "Calendars.Read"
64
+ ]
65
+ },
66
+ {
67
+ "pathPattern": "/me/events",
68
+ "method": "post",
69
+ "toolName": "create-calendar-event",
70
+ "scopes": [
71
+ "Calendars.ReadWrite"
72
+ ]
73
+ },
74
+ {
75
+ "pathPattern": "/me/events/{event-id}",
76
+ "method": "patch",
77
+ "toolName": "update-calendar-event",
78
+ "scopes": [
79
+ "Calendars.ReadWrite"
80
+ ]
81
+ },
82
+ {
83
+ "pathPattern": "/me/events/{event-id}",
84
+ "method": "delete",
85
+ "toolName": "delete-calendar-event",
86
+ "scopes": [
87
+ "Calendars.ReadWrite"
88
+ ]
89
+ },
90
+ {
91
+ "pathPattern": "/me/calendarView",
92
+ "method": "get",
93
+ "toolName": "get-calendar-view",
94
+ "scopes": [
95
+ "Calendars.Read"
96
+ ]
97
+ },
98
+ {
99
+ "pathPattern": "/me/calendars",
100
+ "method": "get",
101
+ "toolName": "list-calendars",
102
+ "scopes": [
103
+ "Calendars.Read"
104
+ ]
105
+ },
106
+ {
107
+ "pathPattern": "/drives",
108
+ "method": "get",
109
+ "toolName": "list-drives",
110
+ "scopes": [
111
+ "Files.Read"
112
+ ]
113
+ },
114
+ {
115
+ "pathPattern": "/drives/{drive-id}/root",
116
+ "method": "get",
117
+ "toolName": "get-drive-root-item",
118
+ "scopes": [
119
+ "Files.Read"
120
+ ]
121
+ },
122
+ {
123
+ "pathPattern": "/drives/{drive-id}/root",
124
+ "method": "get",
125
+ "toolName": "get-root-folder",
126
+ "scopes": [
127
+ "Files.Read"
128
+ ]
129
+ },
130
+ {
131
+ "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/children",
132
+ "method": "get",
133
+ "toolName": "list-folder-files",
134
+ "scopes": [
135
+ "Files.Read"
136
+ ]
137
+ },
138
+ {
139
+ "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/children/{driveItem-id1}/content",
140
+ "method": "get",
141
+ "toolName": "download-onedrive-file-content",
142
+ "scopes": [
143
+ "Files.Read"
144
+ ]
145
+ },
146
+ {
147
+ "pathPattern": "/drives/{drive-id}/items/{driveItem-id}",
148
+ "method": "delete",
149
+ "toolName": "delete-onedrive-file",
150
+ "scopes": [
151
+ "Files.ReadWrite"
152
+ ]
153
+ },
154
+ {
155
+ "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/charts/add",
156
+ "method": "post",
157
+ "toolName": "create-excel-chart",
158
+ "isExcelOp": true,
159
+ "scopes": [
160
+ "Files.ReadWrite"
161
+ ]
162
+ },
163
+ {
164
+ "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/range()/format",
165
+ "method": "patch",
166
+ "toolName": "format-excel-range",
167
+ "isExcelOp": true,
168
+ "scopes": [
169
+ "Files.ReadWrite"
170
+ ]
171
+ },
172
+ {
173
+ "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/range()/sort",
174
+ "method": "patch",
175
+ "toolName": "sort-excel-range",
176
+ "isExcelOp": true,
177
+ "scopes": [
178
+ "Files.ReadWrite"
179
+ ]
180
+ },
181
+ {
182
+ "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/range(address='{address}')",
183
+ "method": "get",
184
+ "toolName": "get-excel-range",
185
+ "isExcelOp": true,
186
+ "scopes": [
187
+ "Files.Read"
188
+ ]
189
+ },
190
+ {
191
+ "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets",
192
+ "method": "get",
193
+ "toolName": "list-excel-worksheets",
194
+ "isExcelOp": true,
195
+ "scopes": [
196
+ "Files.Read"
197
+ ]
198
+ },
199
+ {
200
+ "pathPattern": "/me/onenote/notebooks",
201
+ "method": "get",
202
+ "toolName": "list-onenote-notebooks",
203
+ "scopes": [
204
+ "Notes.Read"
205
+ ]
206
+ },
207
+ {
208
+ "pathPattern": "/me/onenote/notebooks/{notebook-id}/sections",
209
+ "method": "get",
210
+ "toolName": "list-onenote-notebook-sections",
211
+ "scopes": [
212
+ "Notes.Read"
213
+ ]
214
+ },
215
+ {
216
+ "pathPattern": "/me/onenote/notebooks/{notebook-id}/sections/{onenoteSection-id}/pages",
217
+ "method": "get",
218
+ "toolName": "list-onenote-section-pages",
219
+ "scopes": [
220
+ "Notes.Read"
221
+ ]
222
+ },
223
+ {
224
+ "pathPattern": "/me/onenote/pages/{onenotePage-id}/content",
225
+ "method": "get",
226
+ "toolName": "get-onenote-page-content",
227
+ "scopes": [
228
+ "Notes.Read"
229
+ ]
230
+ },
231
+ {
232
+ "pathPattern": "/me/onenote/pages",
233
+ "method": "post",
234
+ "toolName": "create-onenote-page",
235
+ "scopes": [
236
+ "Notes.Create"
237
+ ]
238
+ },
239
+ {
240
+ "pathPattern": "/me/todo/lists",
241
+ "method": "get",
242
+ "toolName": "list-todo-task-lists",
243
+ "scopes": [
244
+ "Tasks.Read"
245
+ ]
246
+ },
247
+ {
248
+ "pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks",
249
+ "method": "get",
250
+ "toolName": "list-todo-tasks",
251
+ "scopes": [
252
+ "Tasks.Read"
253
+ ]
254
+ },
255
+ {
256
+ "pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks/{todoTask-id}",
257
+ "method": "get",
258
+ "toolName": "get-todo-task",
259
+ "scopes": [
260
+ "Tasks.Read"
261
+ ]
262
+ },
263
+ {
264
+ "pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks",
265
+ "method": "post",
266
+ "toolName": "create-todo-task",
267
+ "scopes": [
268
+ "Tasks.ReadWrite"
269
+ ]
270
+ },
271
+ {
272
+ "pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks/{todoTask-id}",
273
+ "method": "patch",
274
+ "toolName": "update-todo-task",
275
+ "scopes": [
276
+ "Tasks.ReadWrite"
277
+ ]
278
+ },
279
+ {
280
+ "pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks/{todoTask-id}",
281
+ "method": "delete",
282
+ "toolName": "delete-todo-task",
283
+ "scopes": [
284
+ "Tasks.ReadWrite"
285
+ ]
286
+ },
287
+ {
288
+ "pathPattern": "/me/planner/tasks",
289
+ "method": "get",
290
+ "toolName": "list-planner-tasks",
291
+ "scopes": [
292
+ "Tasks.Read"
293
+ ]
294
+ },
295
+ {
296
+ "pathPattern": "/planner/plans/{plannerPlan-id}",
297
+ "method": "get",
298
+ "toolName": "get-planner-plan",
299
+ "scopes": [
300
+ "Tasks.Read"
301
+ ]
302
+ },
303
+ {
304
+ "pathPattern": "/planner/plans/{plannerPlan-id}/tasks",
305
+ "method": "get",
306
+ "toolName": "list-plan-tasks",
307
+ "scopes": [
308
+ "Tasks.Read"
309
+ ]
310
+ },
311
+ {
312
+ "pathPattern": "/planner/tasks/{plannerTask-id}",
313
+ "method": "get",
314
+ "toolName": "get-planner-task",
315
+ "scopes": [
316
+ "Tasks.Read"
317
+ ]
318
+ },
319
+ {
320
+ "pathPattern": "/planner/tasks",
321
+ "method": "post",
322
+ "toolName": "create-planner-task",
323
+ "scopes": [
324
+ "Tasks.ReadWrite"
325
+ ]
326
+ },
327
+ {
328
+ "pathPattern": "/me/contacts",
329
+ "method": "get",
330
+ "toolName": "list-outlook-contacts",
331
+ "scopes": [
332
+ "Contacts.Read"
333
+ ]
334
+ },
335
+ {
336
+ "pathPattern": "/me/contacts/{contact-id}",
337
+ "method": "get",
338
+ "toolName": "get-outlook-contact",
339
+ "scopes": [
340
+ "Contacts.Read"
341
+ ]
342
+ },
343
+ {
344
+ "pathPattern": "/me/contacts",
345
+ "method": "post",
346
+ "toolName": "create-outlook-contact",
347
+ "scopes": [
348
+ "Contacts.ReadWrite"
349
+ ]
350
+ },
351
+ {
352
+ "pathPattern": "/me/contacts/{contact-id}",
353
+ "method": "patch",
354
+ "toolName": "update-outlook-contact",
355
+ "scopes": [
356
+ "Contacts.ReadWrite"
357
+ ]
358
+ },
359
+ {
360
+ "pathPattern": "/me/contacts/{contact-id}",
361
+ "method": "delete",
362
+ "toolName": "delete-outlook-contact",
363
+ "scopes": [
364
+ "Contacts.ReadWrite"
365
+ ]
366
+ },
367
+ {
368
+ "pathPattern": "/me",
369
+ "method": "get",
370
+ "toolName": "get-current-user",
371
+ "scopes": [
372
+ "User.Read"
373
+ ]
374
+ }
375
+ ]
@@ -0,0 +1,51 @@
1
+ # MS 365 OpenAPI Client Generation
2
+
3
+ This directory contains the generated TypeScript client for the Microsoft 365 API based on the OpenAPI specification.
4
+
5
+ ## The Evolution
6
+
7
+ ### Initial Challenge
8
+
9
+ Our initial approach used the full MS 365 OpenAPI specification file directly. This created two significant problems:
10
+
11
+ - The spec file was a whopping 45MB in size
12
+ - It had to be included in the npm package
13
+ - Startup time was painfully slow due to parsing the large spec file
14
+
15
+ ### Exploration Phase
16
+
17
+ We explored several alternatives:
18
+
19
+ 1. Live-parsing a trimmed version of the spec file
20
+ 2. Creating a static trimmed version
21
+ 3. Pre-generating client code
22
+
23
+ ### Current Solution
24
+
25
+ We eventually settled on a combined approach:
26
+
27
+ - Trim the OpenAPI spec to only what we need
28
+ - Generate static TypeScript client code using [openapi-zod-client](https://github.com/astahmer/openapi-zod-client)
29
+
30
+ ### Benefits
31
+
32
+ - **Dramatically faster startup time** - No need to parse a large spec file
33
+ - **Significantly smaller package size** - No more bundling a 45MB spec file
34
+ - **Type safety** - Full TypeScript types generated from the OpenAPI spec
35
+ - **Validation** - Zod schemas for request/response validation
36
+
37
+ ## Regenerating the Client
38
+
39
+ To regenerate the client code (e.g., after API changes or to update the supported endpoints):
40
+
41
+ ```
42
+ npm run bin/generate-graph-client.mjs
43
+ ```
44
+
45
+ This command does the following:
46
+
47
+ 1. Fetches/processes the OpenAPI spec
48
+ 2. Generates the TypeScript client with Zod validation
49
+ 3. Outputs the result to `client.ts` in this directory
50
+
51
+ No complex build scripts needed - the generation is handled by openapi-zod-client.