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

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
@@ -5,7 +5,7 @@ global.fetch = vi.fn();
5
5
  describe('Graph API Functions', () => {
6
6
  beforeEach(() => {
7
7
  vi.clearAllMocks();
8
- global.fetch.mockImplementation(async () => ({
8
+ (global.fetch as jest.Mock).mockImplementation(async () => ({
9
9
  ok: true,
10
10
  status: 200,
11
11
  json: async () => ({ value: 'test data' }),
@@ -18,7 +18,7 @@ describe('Graph API Functions', () => {
18
18
  });
19
19
 
20
20
  describe('createSession', () => {
21
- async function createSession(filePath, token) {
21
+ async function createSession(filePath: string, token: string): Promise<string | null> {
22
22
  try {
23
23
  const response = await fetch(
24
24
  `https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/createSession`,
@@ -44,7 +44,7 @@ describe('Graph API Functions', () => {
44
44
  }
45
45
 
46
46
  it('should create a session successfully', async () => {
47
- global.fetch.mockImplementationOnce(async () => ({
47
+ (global.fetch as jest.Mock).mockImplementationOnce(async () => ({
48
48
  ok: true,
49
49
  status: 200,
50
50
  json: async () => ({ id: 'session-123' }),
@@ -65,7 +65,7 @@ describe('Graph API Functions', () => {
65
65
  });
66
66
 
67
67
  it('should return null if session creation fails', async () => {
68
- global.fetch.mockImplementationOnce(async () => ({
68
+ (global.fetch as jest.Mock).mockImplementationOnce(async () => ({
69
69
  ok: false,
70
70
  status: 400,
71
71
  text: async () => 'Bad request',
@@ -77,7 +77,7 @@ describe('Graph API Functions', () => {
77
77
  });
78
78
 
79
79
  it('should return null if an error is thrown', async () => {
80
- global.fetch.mockImplementationOnce(() => {
80
+ (global.fetch as jest.Mock).mockImplementationOnce(() => {
81
81
  throw new Error('Network error');
82
82
  });
83
83
 
@@ -0,0 +1,17 @@
1
+ import { Endpoint } from './src/generated/endpoint-types.js';
2
+ import { Zodios } from './src/generated/hack.js';
3
+
4
+ const testEndpoint: Endpoint = {
5
+ method: 'get',
6
+ path: '/me/mailFolders/{mailFolder-id}/messages',
7
+ alias: 'test-endpoint',
8
+ description: 'Test endpoint',
9
+ requestFormat: 'json',
10
+ parameters: [],
11
+ response: {} as any,
12
+ };
13
+
14
+ const zodios = new Zodios([testEndpoint]);
15
+
16
+ console.log('Parameters after processing:');
17
+ console.log(JSON.stringify(zodios.endpoints[0].parameters, null, 2));
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "NodeNext",
5
+ "outDir": "dist",
6
+ "rootDir": "src",
7
+ "strict": true,
8
+ "resolveJsonModule": true
9
+ },
10
+ "include": [
11
+ "src/**/*"
12
+ ],
13
+ "exclude": [
14
+ "test/**/*"
15
+ ]
16
+ }
@@ -1,442 +0,0 @@
1
- import logger from './logger.mjs';
2
- import {
3
- buildParameterSchemas,
4
- buildRequestUrl,
5
- findPathAndOperation,
6
- isMethodWithBody,
7
- loadOpenApiSpec,
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
- }
34
-
35
- export const TARGET_ENDPOINTS = [
36
- {
37
- pathPattern: '/me/messages',
38
- method: 'get',
39
- toolName: 'list-mail-messages',
40
- scopes: ['Mail.Read'],
41
- },
42
- {
43
- pathPattern: '/me/mailFolders',
44
- method: 'get',
45
- toolName: 'list-mail-folders',
46
- scopes: ['Mail.Read'],
47
- },
48
- {
49
- pathPattern: '/me/mailFolders/{mailFolder-id}/messages',
50
- method: 'get',
51
- toolName: 'list-mail-folder-messages',
52
- scopes: ['Mail.Read'],
53
- },
54
- {
55
- pathPattern: '/me/messages/{message-id}',
56
- method: 'get',
57
- toolName: 'get-mail-message',
58
- scopes: ['Mail.Read'],
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
-
73
- {
74
- pathPattern: '/me/events',
75
- method: 'get',
76
- toolName: 'list-calendar-events',
77
- scopes: ['Calendars.Read'],
78
- },
79
- {
80
- pathPattern: '/me/events/{event-id}',
81
- method: 'get',
82
- toolName: 'get-calendar-event',
83
- scopes: ['Calendars.Read'],
84
- },
85
- {
86
- pathPattern: '/me/events',
87
- method: 'post',
88
- toolName: 'create-calendar-event',
89
- scopes: ['Calendars.ReadWrite'],
90
- },
91
- {
92
- pathPattern: '/me/events/{event-id}',
93
- method: 'patch',
94
- toolName: 'update-calendar-event',
95
- scopes: ['Calendars.ReadWrite'],
96
- },
97
- {
98
- pathPattern: '/me/events/{event-id}',
99
- method: 'delete',
100
- toolName: 'delete-calendar-event',
101
- scopes: ['Calendars.ReadWrite'],
102
- },
103
- {
104
- pathPattern: '/me/calendarView',
105
- method: 'get',
106
- toolName: 'get-calendar-view',
107
- scopes: ['Calendars.Read'],
108
- },
109
- {
110
- pathPattern: '/me/calendars',
111
- method: 'get',
112
- toolName: 'list-calendars',
113
- scopes: ['Calendars.Read'],
114
- },
115
-
116
- {
117
- pathPattern: '/drives',
118
- method: 'get',
119
- toolName: 'list-drives',
120
- scopes: ['Files.Read'],
121
- },
122
- {
123
- pathPattern: '/drives/{drive-id}/root',
124
- method: 'get',
125
- toolName: 'get-drive-root-item',
126
- scopes: ['Files.Read'],
127
- },
128
- {
129
- pathPattern: '/drives/{drive-id}/root',
130
- method: 'get',
131
- toolName: 'get-root-folder',
132
- scopes: ['Files.Read'],
133
- },
134
- {
135
- pathPattern: '/drives/{drive-id}/items/{driveItem-id}/children',
136
- method: 'get',
137
- toolName: 'list-folder-files',
138
- scopes: ['Files.Read'],
139
- },
140
- {
141
- pathPattern: '/drives/{drive-id}/items/{driveItem-id}/children',
142
- method: 'post',
143
- toolName: 'create-item-in-folder',
144
- scopes: ['Files.ReadWrite'],
145
- },
146
- {
147
- pathPattern: '/drives/{drive-id}/items/{driveItem-id}/children/{driveItem-id1}/content',
148
- method: 'get',
149
- toolName: 'download-onedrive-file-content',
150
- scopes: ['Files.Read'],
151
- },
152
- {
153
- pathPattern: '/drives/{drive-id}/items/{driveItem-id}',
154
- method: 'delete',
155
- toolName: 'delete-onedrive-file',
156
- scopes: ['Files.ReadWrite'],
157
- },
158
- {
159
- pathPattern: '/drives/{drive-id}/items/{driveItem-id}',
160
- method: 'patch',
161
- toolName: 'update-onedrive-file-metadata',
162
- scopes: ['Files.ReadWrite'],
163
- },
164
-
165
- {
166
- pathPattern:
167
- '/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/charts/add',
168
- method: 'post',
169
- toolName: 'create-excel-chart',
170
- isExcelOp: true,
171
- scopes: ['Files.ReadWrite'],
172
- },
173
- {
174
- pathPattern:
175
- '/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/range()/format',
176
- method: 'patch',
177
- toolName: 'format-excel-range',
178
- isExcelOp: true,
179
- scopes: ['Files.ReadWrite'],
180
- },
181
- {
182
- pathPattern:
183
- '/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/range()/sort',
184
- method: 'patch',
185
- toolName: 'sort-excel-range',
186
- isExcelOp: true,
187
- scopes: ['Files.ReadWrite'],
188
- },
189
- {
190
- pathPattern:
191
- "/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/range(address='{address}')",
192
- method: 'get',
193
- toolName: 'get-excel-range',
194
- isExcelOp: true,
195
- scopes: ['Files.Read'],
196
- },
197
- {
198
- pathPattern: '/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets',
199
- method: 'get',
200
- toolName: 'list-excel-worksheets',
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'],
340
- },
341
- ];
342
-
343
- export async function registerDynamicTools(server, graphClient) {
344
- try {
345
- const openapi = loadOpenApiSpec();
346
- logger.info('Generating dynamic tools from OpenAPI spec...');
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
-
358
- for (const endpoint of TARGET_ENDPOINTS) {
359
- const result = findPathAndOperation(openapi, endpoint.pathPattern, endpoint.method);
360
- if (!result) continue;
361
-
362
- const { operation } = result;
363
-
364
- logger.info(
365
- `Creating tool ${endpoint.toolName} for ${endpoint.method.toUpperCase()} ${endpoint.pathPattern}`
366
- );
367
-
368
- const paramsSchema = buildParameterSchemas(endpoint, operation);
369
-
370
- if (endpoint.hasCustomParams) {
371
- if (endpoint.toolName === 'upload-onedrive-file') {
372
- paramsSchema.content = z.string().describe('File content to upload');
373
- paramsSchema.contentType = z
374
- .string()
375
- .optional()
376
- .describe('Content type of the file (e.g., "application/pdf", "image/jpeg")');
377
- } else if (endpoint.toolName === 'create-onedrive-folder') {
378
- paramsSchema.name = z.string().describe('Name of the folder to create');
379
- paramsSchema.description = z.string().optional().describe('Description of the folder');
380
- }
381
- }
382
-
383
- const pathParams = endpoint.pathPattern.match(/\{([^}]+)}/g) || [];
384
-
385
- const handler = async (params) => {
386
- if (endpoint.isExcelOp && !params.filePath) {
387
- return {
388
- content: [
389
- {
390
- type: 'text',
391
- text: JSON.stringify({
392
- error: 'filePath parameter is required for Excel operations',
393
- }),
394
- },
395
- ],
396
- };
397
- }
398
-
399
- const options = {
400
- method: endpoint.method.toUpperCase(),
401
- };
402
-
403
- if (endpoint.isExcelOp) {
404
- options.excelFile = params.filePath;
405
- }
406
-
407
- if (endpoint.toolName === 'download-onedrive-file-content') {
408
- options.rawResponse = true;
409
- }
410
-
411
- const url = buildRequestUrl(endpoint.pathPattern, params, pathParams, operation.parameters);
412
-
413
- if (endpoint.toolName === 'upload-onedrive-file' && params.content) {
414
- options.body = params.content;
415
- options.headers = {
416
- 'Content-Type': params.contentType || 'application/octet-stream',
417
- };
418
- } else if (endpoint.toolName === 'create-folder' && params.name) {
419
- options.body = JSON.stringify({
420
- name: params.name,
421
- folder: {},
422
- '@microsoft.graph.conflictBehavior': 'rename',
423
- ...(params.description && { description: params.description }),
424
- });
425
- options.headers = {
426
- 'Content-Type': 'application/json',
427
- };
428
- } else if (isMethodWithBody(endpoint.method.toLowerCase()) && params.body) {
429
- options.body = JSON.stringify(params.body);
430
- }
431
-
432
- return graphClient.graphRequest(url, options);
433
- };
434
-
435
- server.tool(endpoint.toolName, paramsSchema, handler);
436
- }
437
- logger.info(`Dynamic tools registration complete.`);
438
- } catch (error) {
439
- logger.error('Error registering dynamic tools:', error);
440
- throw error;
441
- }
442
- }
@@ -1,187 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import yaml from 'js-yaml';
4
- import { fileURLToPath } from 'url';
5
- import { z } from 'zod';
6
- import logger from './logger.mjs';
7
- import { createFriendlyParamName, registerParamMapping } from './param-mapper.mjs';
8
-
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = path.dirname(__filename);
11
-
12
- export const OPENAPI_PATH = path.join(__dirname, '..', 'openapi', 'openapi.yaml');
13
-
14
- export function loadOpenApiSpec() {
15
- try {
16
- logger.info('Loading OpenAPI spec...');
17
- const openapiContent = fs.readFileSync(OPENAPI_PATH, 'utf8');
18
- return yaml.load(openapiContent);
19
- } catch (error) {
20
- logger.error('Error loading OpenAPI spec:', error);
21
- throw error;
22
- }
23
- }
24
-
25
- export function mapToZodType(schema) {
26
- if (!schema) return z.any();
27
-
28
- if (schema.$ref) {
29
- const refName = schema.$ref.split('/').pop();
30
- if (refName.toLowerCase().includes('string')) return z.string();
31
- if (refName.toLowerCase().includes('int') || refName.toLowerCase().includes('number'))
32
- return z.number();
33
- if (refName.toLowerCase().includes('boolean')) return z.boolean();
34
- if (refName.toLowerCase().includes('date')) return z.string();
35
- if (refName.toLowerCase().includes('object')) return z.object({}).passthrough();
36
- if (refName.toLowerCase().includes('array')) return z.array(z.any());
37
-
38
- return z.object({}).passthrough();
39
- }
40
-
41
- switch (schema.type) {
42
- case 'string':
43
- const stringSchema = z.string();
44
- if (schema.format === 'date-time') return stringSchema;
45
- if (schema.enum) return z.enum(schema.enum);
46
- return stringSchema;
47
- case 'integer':
48
- case 'number':
49
- return z.number();
50
- case 'boolean':
51
- return z.boolean();
52
- case 'array':
53
- return z.array(mapToZodType(schema.items || {}));
54
- case 'object':
55
- const properties = schema.properties || {};
56
- const shape = {};
57
-
58
- Object.entries(properties).forEach(([key, prop]) => {
59
- shape[key] = mapToZodType(prop);
60
- if (schema.required && schema.required.includes(key)) {
61
- } else {
62
- shape[key] = shape[key].optional();
63
- }
64
- });
65
-
66
- return z.object(shape).passthrough();
67
- default:
68
- return z.any();
69
- }
70
- }
71
-
72
- export function processParameter(parameter) {
73
- const zodSchema = mapToZodType(parameter.schema);
74
-
75
- let schema = parameter.description ? zodSchema.describe(parameter.description) : zodSchema;
76
-
77
- if (!parameter.required) {
78
- schema = schema.optional();
79
- }
80
-
81
- return schema;
82
- }
83
-
84
- export function findPathAndOperation(openapi, pathPattern, method) {
85
- const path = openapi.paths[pathPattern];
86
-
87
- if (!path) {
88
- logger.warn(`Path ${pathPattern} not found in OpenAPI spec`);
89
- return null;
90
- }
91
-
92
- const operation = path[method.toLowerCase()];
93
-
94
- if (!operation) {
95
- logger.warn(`Method ${method} not found for path ${pathPattern}`);
96
- return null;
97
- }
98
-
99
- return { path, operation };
100
- }
101
-
102
- export function isMethodWithBody(method) {
103
- return ['post', 'put', 'patch'].includes(method);
104
- }
105
-
106
- export function buildParameterSchemas(endpoint, operation) {
107
- const paramsSchema = {};
108
-
109
- const pathParams = endpoint.pathPattern.match(/\{([^}]+)}/g) || [];
110
- pathParams.forEach((param) => {
111
- const paramName = param.slice(1, -1);
112
- paramsSchema[paramName] = z.string().describe(`Path parameter: ${paramName}`);
113
- });
114
-
115
- if (operation.parameters) {
116
- operation.parameters.forEach((param) => {
117
- if (param.in === 'query') {
118
- if (!pathParams.includes(`{${param.name}}`)) {
119
- const friendlyName = createFriendlyParamName(param.name);
120
- registerParamMapping(endpoint.toolName, friendlyName, param.name);
121
- paramsSchema[friendlyName] = processParameter(param);
122
- }
123
- }
124
- });
125
- }
126
-
127
- if (isMethodWithBody(endpoint.method) && operation.requestBody) {
128
- const contentType =
129
- operation.requestBody.content?.['application/json'] ||
130
- operation.requestBody.content?.['*/*'] ||
131
- {};
132
-
133
- if (contentType.schema) {
134
- paramsSchema.body = z
135
- .object({})
136
- .passthrough()
137
- .describe(operation.requestBody.description || 'Request body');
138
- }
139
- }
140
-
141
- if (endpoint.isExcelOp) {
142
- paramsSchema.filePath = z.string().describe('Path to the Excel file in OneDrive');
143
-
144
- if (endpoint.pathPattern.includes('range(address=')) {
145
- paramsSchema.address = z.string().describe('Excel range address (e.g., "A1:B10")');
146
- }
147
- }
148
-
149
- return paramsSchema;
150
- }
151
-
152
- export function buildRequestUrl(baseUrl, params, pathParams, queryParamDefs) {
153
- let url = baseUrl;
154
-
155
- pathParams.forEach((param) => {
156
- const paramName = param.slice(1, -1);
157
- url = url.replace(param, params[paramName]);
158
- });
159
-
160
- if (url.includes("range(address='{address}')") && params.address) {
161
- url = url.replace('{address}', encodeURIComponent(params.address));
162
- }
163
-
164
- const queryParams = [];
165
-
166
- if (queryParamDefs) {
167
- queryParamDefs.forEach((param) => {
168
- if (param.in === 'query') {
169
- const friendlyName = createFriendlyParamName(param.name);
170
-
171
- if (params[friendlyName] !== undefined) {
172
- if (Array.isArray(params[friendlyName])) {
173
- queryParams.push(`${param.name}=${params[friendlyName].join(',')}`);
174
- } else {
175
- queryParams.push(`${param.name}=${encodeURIComponent(params[friendlyName])}`);
176
- }
177
- }
178
- }
179
- });
180
- }
181
-
182
- if (queryParams.length > 0) {
183
- url += '?' + queryParams.join('&');
184
- }
185
-
186
- return url;
187
- }