@softeria/ms-365-mcp-server 0.3.2 → 0.3.4

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
@@ -1,213 +1,98 @@
1
1
  # ms-365-mcp-server
2
2
 
3
+ ![npm version](https://img.shields.io/npm/v/@softeria/ms-365-mcp-server.svg) ![build status](https://github.com/softeria/ms-365-mcp-server/actions/workflows/build.yml/badge.svg) ![license](https://img.shields.io/badge/license-MIT-blue.svg)
4
+
3
5
  Microsoft 365 MCP Server
4
6
 
5
7
  A Model Context Protocol (MCP) server for interacting with Microsoft 365 services through the Graph API.
6
8
 
7
- [![npm version](https://img.shields.io/npm/v/@softeria/ms-365-mcp-server.svg)](https://www.npmjs.com/package/@softeria/ms-365-mcp-server)
9
+ ## Prerequisites
10
+
11
+ - Node.js >= 14
8
12
 
9
13
  ## Features
10
14
 
11
- - Authentication using Microsoft Authentication Library (MSAL)
15
+ - Authentication via Microsoft Authentication Library (MSAL)
12
16
  - Excel file operations
13
17
  - Calendar event management
14
18
  - Mail operations
15
19
  - OneDrive file management
16
- - Dynamic tools powered by Microsoft Graph OpenAPI specification
20
+ - Microsoft Teams integration
21
+ - OneNote notebooks and pages
22
+ - To Do tasks and task lists
23
+ - Planner plans and tasks
24
+ - SharePoint sites and lists
25
+ - Outlook contacts
26
+ - User and group management
27
+ - Dynamic tools powered by Microsoft Graph OpenAPI spec
17
28
  - Built on the Model Context Protocol
18
29
 
19
- ## Installation
20
-
21
- ```bash
22
- npx @softeria/ms-365-mcp-server
23
- ```
24
-
25
30
  ## Quick Start Example
26
31
 
27
- Login and test authentication in Claude Desktop:
32
+ Test login in Claude Desktop:
28
33
 
29
- ![MS 365 MCP Server login example in Claude Desktop](https://github.com/user-attachments/assets/936d16bc-b3e1-437b-b3f1-03c54874a816)
34
+ ![Login example](![Image](https://github.com/user-attachments/assets/e457884f-c98a-4186-9e6f-eb323ec24e0a)
30
35
 
31
36
  ## Examples
32
37
 
33
38
  ![Image](https://github.com/user-attachments/assets/1a296afb-48ed-42b0-9e7c-e685d5d1784c)
34
39
 
35
-
36
- ## Integration with Claude
40
+ ## Integration
37
41
 
38
42
  ### Claude Desktop
39
43
 
40
44
  To add this MCP server to Claude Desktop:
41
45
 
42
- 1. Launch Claude Desktop
43
- 2. Go to Settings > MCPs
44
- 3. Click "Add MCP"
45
- 4. Set the following configuration:
46
- - Name: `ms365` (or any name you prefer)
47
- - Command: `npx @softeria/ms-365-mcp-server`
48
- - Click "Add"
49
-
50
- Alternatively, you can edit Claude Desktop's configuration file directly. The location varies by platform, but you can
51
- find it by going to Settings > Developer > Edit Config. Add this to your configuration file:
46
+ Edit the config file under Settings > Developer:
52
47
 
53
48
  ```json
54
49
  {
55
50
  "mcpServers": {
56
51
  "ms365": {
57
52
  "command": "npx",
58
- "args": ["-y", "@softeria/ms-365-mcp-server"]
53
+ "args": [
54
+ "-y",
55
+ "@softeria/ms-365-mcp-server"
56
+ ]
59
57
  }
60
58
  }
61
59
  }
62
60
  ```
63
61
 
64
- ### Using Claude Code CLI
65
-
66
- You can add the server to Claude Code CLI using this command:
62
+ ### Claude Code CLI
67
63
 
68
64
  ```bash
69
65
  claude mcp add ms365 -- npx -y @softeria/ms-365-mcp-server
70
66
  ```
71
67
 
72
- For other Claude interfaces that support MCPs, please refer to their respective documentation for the correct
68
+ For other interfaces that support MCPs, please refer to their respective documentation for the correct
73
69
  integration method.
74
70
 
75
- ## Usage
76
-
77
- ### Command Line Options
78
-
79
- ```bash
80
- npx @softeria/ms-365-mcp-server [options]
81
- ```
82
-
83
- Options:
84
-
85
- - `--login`: Force login using device code flow and verify Graph API access
86
- - `--logout`: Log out and clear saved credentials
87
- - `--verify-login`: Test current authentication and verify Graph API access without starting the server
88
- - `-v`: Enable verbose logging
89
-
90
71
  ### Authentication
91
72
 
92
- **Important:** You must authenticate before using the MCP server. There are two ways to authenticate:
93
-
94
- 1. Running the server with the `--login` flag:
73
+ > ⚠️ You must authenticate before using tools.
95
74
 
75
+ 1. **MCP client login**:
76
+ - Call the `login` tool (auto-checks existing token)
77
+ - If needed, get URL+code, visit in browser
78
+ - Use `verify-login` tool to confirm
79
+ -
80
+ 2. **Optional CLI login**:
96
81
  ```bash
97
82
  npx @softeria/ms-365-mcp-server --login
98
83
  ```
84
+ Follow the URL and code prompt in terminal.
99
85
 
100
- This will display the login URL and code in the terminal.
101
-
102
- 2. When using Claude Code or other MCP clients, use the login tools:
103
- - First use the `login` tool, which will automatically check if you're already logged in
104
- - If not already logged in, it will return the login URL and code
105
- - Visit the URL and enter the code in your browser
106
- - Then use the `verify-login` tool to check if the login was successful
107
- - To force a new login even if already authenticated, use the `login` tool with `force: true`
108
-
109
- Both methods trigger the device code flow authentication, but they handle the UI interaction differently:
86
+ Tokens are cached securely in your OS credential store (fallback to file).
110
87
 
111
- - CLI version displays the instructions directly in the terminal
112
- - MCP tool version returns the instructions as data that can be shown in the client UI
113
-
114
- You can verify your authentication status with the `--verify-login` flag, which will check if your token can successfully
115
- fetch user data from Microsoft Graph API:
116
-
117
- ```bash
118
- npx @softeria/ms-365-mcp-server --verify-login
119
- ```
120
-
121
- Both `--login` and `--verify-login` will return a JSON response that includes your basic user information from Microsoft
122
- Graph API if authentication is successful:
123
-
124
- ```json
125
- {
126
- "success": true,
127
- "message": "Login successful",
128
- "userData": {
129
- "displayName": "Your Name",
130
- "userPrincipalName": "your.email@example.com"
131
- }
132
- }
133
- ```
88
+ ## Tools
134
89
 
135
- Authentication tokens are cached securely in your system's credential store with fallback to file storage if needed.
136
-
137
- ### MCP Tools
138
-
139
- This server provides several MCP tools for interacting with Microsoft 365 services, including:
140
-
141
- - Authentication (login, logout)
142
- - Files/OneDrive management
143
- - Excel operations:
144
- - List worksheets
145
- - Get cell range values
146
- - Format cell ranges
147
- - Sort data
148
- - Create charts
149
- - Calendar management
150
- - Mail operations
151
-
152
- For a complete list of available tools and their parameters, use an MCP-enabled Claude interface and explore the available tools.
153
-
154
- ## For Developers
155
-
156
- ### Setup
157
-
158
- ```bash
159
- # Clone the repository
160
- git clone https://github.com/softeria/ms-365-mcp-server.git
161
- cd ms-365-mcp-server
162
-
163
- # Install dependencies
164
- npm install
165
-
166
- # Run tests
167
- npm test
168
- ```
169
-
170
- ### OpenAPI Integration
171
-
172
- This project uses the Microsoft Graph OpenAPI specification to dynamically generate MCP tools. During installation, the OpenAPI specification is automatically downloaded from Microsoft Graph's GitHub repository.
173
-
174
- To manually download the latest OpenAPI spec:
175
-
176
- ```bash
177
- # Download the latest OpenAPI spec from Microsoft Graph
178
- npm run download-openapi
179
- ```
180
-
181
- ### GitHub Actions
182
-
183
- This repository uses GitHub Actions for continuous integration and deployment:
184
-
185
- - **Build Workflow**: Runs on all pushes to main and pull requests. Verifies the project builds successfully and passes
186
- all tests.
187
- - **Publish Workflow**: Automatically publishes to npm when a new GitHub release is created.
188
-
189
- [![Build Status](https://github.com/softeria/ms-365-mcp-server/actions/workflows/build.yml/badge.svg)](https://github.com/softeria/ms-365-mcp-server/actions/workflows/build.yml)
190
-
191
- ### Release Process
192
-
193
- To create a new release:
194
-
195
- ```bash
196
- # Default (patch version): 0.1.11 -> 0.1.12
197
- npm run release
198
-
199
- # Minor version: 0.1.11 -> 0.2.0
200
- npm run release minor
201
-
202
- # Major version: 0.1.11 -> 1.0.0
203
- npm run release major
204
- ```
90
+ - **Authentication:** `login`, `logout`, `verify-login`
91
+ - **Excel:** list worksheets, get/set ranges, format, sort, chart
92
+ - **Calendar:** list/create/update/delete events
93
+ - **Mail:** send, read, delete messages
94
+ - **OneDrive:** upload, download, list files
205
95
 
206
- This script will:
96
+ ## License
207
97
 
208
- 1. Run tests to verify everything works
209
- 2. Bump the version number according to the specified type (patch by default)
210
- 3. Commit the version changes
211
- 4. Push to GitHub
212
- 5. Create a GitHub release
213
- 6. Trigger the publishing workflow to publish to npm
98
+ 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.2",
3
+ "version": "0.3.4",
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
+ });