@softeria/ms-365-mcp-server 0.3.4 → 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.
- package/.github/workflows/build.yml +3 -0
- package/.github/workflows/npm-publish.yml +2 -0
- package/README.md +4 -15
- package/bin/generate-graph-client.mjs +59 -0
- package/bin/{download-openapi.mjs → modules/download-openapi.mjs} +10 -20
- package/bin/modules/extract-descriptions.mjs +48 -0
- package/bin/modules/generate-mcp-tools.mjs +36 -0
- package/bin/modules/simplified-openapi.mjs +78 -0
- package/dist/auth-tools.js +80 -0
- package/dist/auth.js +219 -0
- package/dist/cli.js +21 -0
- package/dist/endpoints.json +375 -0
- package/dist/generated/client.js +14683 -0
- package/dist/generated/endpoint-types.js +1 -0
- package/dist/generated/hack.js +37 -0
- package/dist/graph-client.js +254 -0
- package/dist/graph-tools.js +98 -0
- package/dist/index.js +39 -0
- package/dist/logger.js +33 -0
- package/dist/server.js +32 -0
- package/{src/version.mjs → dist/version.js} +0 -2
- package/package.json +12 -9
- package/src/{auth-tools.mjs → auth-tools.ts} +7 -5
- package/src/{auth.mjs → auth.ts} +60 -30
- package/src/{cli.mjs → cli.ts} +9 -1
- package/src/endpoints.json +375 -0
- package/src/generated/README.md +51 -0
- package/src/generated/client.ts +24916 -0
- package/src/generated/endpoint-types.ts +27 -0
- package/src/generated/hack.ts +50 -0
- package/src/{graph-client.mjs → graph-client.ts} +53 -18
- package/src/graph-tools.ts +174 -0
- package/{index.mjs → src/index.ts} +6 -6
- package/src/{logger.mjs → logger.ts} +1 -1
- package/src/{server.mjs → server.ts} +16 -9
- package/src/version.ts +9 -0
- package/test/{auth-tools.test.js → auth-tools.test.ts} +41 -38
- package/test/{cli.test.js → cli.test.ts} +3 -3
- package/test/{graph-api.test.js → graph-api.test.ts} +5 -5
- package/test/test-hack.ts +17 -0
- package/tsconfig.json +16 -0
- package/src/dynamic-tools.mjs +0 -442
- package/src/openapi-helpers.mjs +0 -187
- package/src/param-mapper.mjs +0 -30
- package/test/dynamic-tools.test.js +0 -852
- package/test/mappings.test.js +0 -29
- package/test/mcp-server.test.js +0 -36
- package/test/openapi-helpers.test.js +0 -210
- package/test/param-mapper.test.js +0 -56
package/src/{auth.mjs → auth.ts}
RENAMED
|
@@ -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.
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
}
|
package/src/{cli.mjs → cli.ts}
RENAMED
|
@@ -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
|
|
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.
|