@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 +41 -156
- package/package.json +1 -1
- package/src/auth.mjs +29 -9
- package/src/dynamic-tools.mjs +225 -14
- package/test/mappings.test.js +29 -0
package/README.md
CHANGED
|
@@ -1,213 +1,98 @@
|
|
|
1
1
|
# ms-365-mcp-server
|
|
2
2
|
|
|
3
|
+
  
|
|
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
|
-
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
- Node.js >= 14
|
|
8
12
|
|
|
9
13
|
## Features
|
|
10
14
|
|
|
11
|
-
- Authentication
|
|
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
|
-
-
|
|
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
|
-
|
|
32
|
+
Test login in Claude Desktop:
|
|
28
33
|
|
|
29
|
-

|
|
30
35
|
|
|
31
36
|
## Examples
|
|
32
37
|
|
|
33
38
|

|
|
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
|
-
|
|
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": [
|
|
53
|
+
"args": [
|
|
54
|
+
"-y",
|
|
55
|
+
"@softeria/ms-365-mcp-server"
|
|
56
|
+
]
|
|
59
57
|
}
|
|
60
58
|
}
|
|
61
59
|
}
|
|
62
60
|
```
|
|
63
61
|
|
|
64
|
-
###
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
[](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
|
-
|
|
96
|
+
## License
|
|
207
97
|
|
|
208
|
-
|
|
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
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
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
|
|
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 =
|
|
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);
|
package/src/dynamic-tools.mjs
CHANGED
|
@@ -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: '/
|
|
110
|
+
pathPattern: '/me/calendars',
|
|
63
111
|
method: 'get',
|
|
64
|
-
toolName: '
|
|
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
|
+
});
|