@softeria/ms-365-mcp-server 0.11.4 → 0.12.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/.releaserc.json +12 -0
- package/README.md +2 -1
- package/bin/modules/simplified-openapi.mjs +465 -274
- package/dist/auth-tools.js +181 -173
- package/dist/auth.js +402 -415
- package/dist/cli.js +35 -36
- package/dist/generated/client.js +6976 -14312
- package/dist/generated/endpoint-types.js +0 -1
- package/dist/generated/hack.js +39 -33
- package/dist/graph-client.js +426 -473
- package/dist/graph-tools.js +217 -228
- package/dist/index.js +76 -79
- package/dist/lib/microsoft-auth.js +62 -72
- package/dist/logger.js +36 -27
- package/dist/oauth-provider.js +48 -47
- package/dist/server.js +277 -264
- package/dist/version.js +9 -6
- package/package.json +13 -3
- package/tsup.config.ts +30 -0
- package/bin/release.mjs +0 -69
package/dist/graph-client.js
CHANGED
|
@@ -1,509 +1,462 @@
|
|
|
1
|
-
import logger from
|
|
2
|
-
import { refreshAccessToken } from
|
|
1
|
+
import logger from "./logger.js";
|
|
2
|
+
import { refreshAccessToken } from "./lib/microsoft-auth.js";
|
|
3
3
|
class GraphClient {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
constructor(authManager) {
|
|
5
|
+
// accountId -> (filePath -> sessionId)
|
|
6
|
+
this.accessToken = null;
|
|
7
|
+
this.refreshToken = null;
|
|
8
|
+
this.authManager = authManager;
|
|
9
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
10
|
+
}
|
|
11
|
+
setOAuthTokens(accessToken, refreshToken) {
|
|
12
|
+
this.accessToken = accessToken;
|
|
13
|
+
this.refreshToken = refreshToken || null;
|
|
14
|
+
}
|
|
15
|
+
async getCurrentAccountId() {
|
|
16
|
+
const currentAccount = await this.authManager.getCurrentAccount();
|
|
17
|
+
return currentAccount?.homeAccountId || null;
|
|
18
|
+
}
|
|
19
|
+
getAccountSessions(accountId) {
|
|
20
|
+
if (!this.sessions.has(accountId)) {
|
|
21
|
+
this.sessions.set(accountId, /* @__PURE__ */ new Map());
|
|
9
22
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
return this.sessions.get(accountId);
|
|
24
|
+
}
|
|
25
|
+
async getSessionForFile(filePath) {
|
|
26
|
+
const accountId = await this.getCurrentAccountId();
|
|
27
|
+
if (!accountId) return null;
|
|
28
|
+
const accountSessions = this.getAccountSessions(accountId);
|
|
29
|
+
return accountSessions.get(filePath) || null;
|
|
30
|
+
}
|
|
31
|
+
async setSessionForFile(filePath, sessionId) {
|
|
32
|
+
const accountId = await this.getCurrentAccountId();
|
|
33
|
+
if (!accountId) return;
|
|
34
|
+
const accountSessions = this.getAccountSessions(accountId);
|
|
35
|
+
accountSessions.set(filePath, sessionId);
|
|
36
|
+
}
|
|
37
|
+
async createSession(filePath) {
|
|
38
|
+
try {
|
|
39
|
+
if (!filePath) {
|
|
40
|
+
logger.error("No file path provided for Excel session");
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const existingSession = await this.getSessionForFile(filePath);
|
|
44
|
+
if (existingSession) {
|
|
45
|
+
return existingSession;
|
|
46
|
+
}
|
|
47
|
+
logger.info(`Creating new Excel session for file: ${filePath}`);
|
|
48
|
+
const accessToken = await this.authManager.getToken();
|
|
49
|
+
const response = await fetch(
|
|
50
|
+
`https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/createSession`,
|
|
51
|
+
{
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: `Bearer ${accessToken}`,
|
|
55
|
+
"Content-Type": "application/json"
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({ persistChanges: true })
|
|
21
58
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const accountSessions = this.getAccountSessions(accountId);
|
|
36
|
-
accountSessions.set(filePath, sessionId);
|
|
59
|
+
);
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const errorText = await response.text();
|
|
62
|
+
logger.error(`Failed to create session: ${response.status} - ${errorText}`);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const result = await response.json();
|
|
66
|
+
logger.info(`Session created successfully for file: ${filePath}`);
|
|
67
|
+
await this.setSessionForFile(filePath, result.id);
|
|
68
|
+
return result.id;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logger.error(`Error creating Excel session: ${error}`);
|
|
71
|
+
return null;
|
|
37
72
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const existingSession = await this.getSessionForFile(filePath);
|
|
45
|
-
if (existingSession) {
|
|
46
|
-
return existingSession;
|
|
47
|
-
}
|
|
48
|
-
logger.info(`Creating new Excel session for file: ${filePath}`);
|
|
49
|
-
const accessToken = await this.authManager.getToken();
|
|
50
|
-
const response = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/createSession`, {
|
|
51
|
-
method: 'POST',
|
|
52
|
-
headers: {
|
|
53
|
-
Authorization: `Bearer ${accessToken}`,
|
|
54
|
-
'Content-Type': 'application/json',
|
|
55
|
-
},
|
|
56
|
-
body: JSON.stringify({ persistChanges: true }),
|
|
57
|
-
});
|
|
58
|
-
if (!response.ok) {
|
|
59
|
-
const errorText = await response.text();
|
|
60
|
-
logger.error(`Failed to create session: ${response.status} - ${errorText}`);
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
const result = await response.json();
|
|
64
|
-
logger.info(`Session created successfully for file: ${filePath}`);
|
|
65
|
-
await this.setSessionForFile(filePath, result.id);
|
|
66
|
-
return result.id;
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
logger.error(`Error creating Excel session: ${error}`);
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
73
|
+
}
|
|
74
|
+
async makeRequest(endpoint, options = {}) {
|
|
75
|
+
let accessToken = options.accessToken || this.accessToken || await this.authManager.getToken();
|
|
76
|
+
let refreshToken = options.refreshToken || this.refreshToken;
|
|
77
|
+
if (!accessToken) {
|
|
78
|
+
throw new Error("No access token available");
|
|
72
79
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
try {
|
|
81
|
+
const response = await this.performRequest(endpoint, accessToken, options);
|
|
82
|
+
if (response.status === 401 && refreshToken) {
|
|
83
|
+
await this.refreshAccessToken(refreshToken);
|
|
84
|
+
accessToken = this.accessToken || accessToken;
|
|
77
85
|
if (!accessToken) {
|
|
78
|
-
|
|
86
|
+
throw new Error("Failed to refresh access token");
|
|
79
87
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
return this.performRequest(endpoint, accessToken, options);
|
|
89
|
+
}
|
|
90
|
+
if (response.status === 403) {
|
|
91
|
+
const errorText = await response.text();
|
|
92
|
+
if (errorText.includes("scope") || errorText.includes("permission")) {
|
|
93
|
+
const hasWorkPermissions = await this.authManager.hasWorkAccountPermissions();
|
|
94
|
+
if (!hasWorkPermissions) {
|
|
95
|
+
logger.info("403 scope error detected, attempting to expand to work account scopes...");
|
|
96
|
+
const expanded = await this.authManager.expandToWorkAccountScopes();
|
|
97
|
+
if (expanded) {
|
|
98
|
+
const newToken = await this.authManager.getToken();
|
|
99
|
+
if (newToken) {
|
|
100
|
+
logger.info("Retrying request with expanded scopes...");
|
|
101
|
+
return this.performRequest(endpoint, newToken, options);
|
|
102
|
+
}
|
|
92
103
|
}
|
|
93
|
-
|
|
94
|
-
const errorText = await response.text();
|
|
95
|
-
if (errorText.includes('scope') || errorText.includes('permission')) {
|
|
96
|
-
const hasWorkPermissions = await this.authManager.hasWorkAccountPermissions();
|
|
97
|
-
if (!hasWorkPermissions) {
|
|
98
|
-
logger.info('403 scope error detected, attempting to expand to work account scopes...');
|
|
99
|
-
const expanded = await this.authManager.expandToWorkAccountScopes();
|
|
100
|
-
if (expanded) {
|
|
101
|
-
const newToken = await this.authManager.getToken();
|
|
102
|
-
if (newToken) {
|
|
103
|
-
logger.info('Retrying request with expanded scopes...');
|
|
104
|
-
return this.performRequest(endpoint, newToken, options);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
throw new Error(`Microsoft Graph API scope error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
110
|
-
}
|
|
111
|
-
if (!response.ok) {
|
|
112
|
-
throw new Error(`Microsoft Graph API error: ${response.status} ${response.statusText}`);
|
|
113
|
-
}
|
|
114
|
-
return response.json();
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
logger.error('Microsoft Graph API request failed:', error);
|
|
118
|
-
throw error;
|
|
104
|
+
}
|
|
119
105
|
}
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Microsoft Graph API scope error: ${response.status} ${response.statusText} - ${errorText}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
throw new Error(`Microsoft Graph API error: ${response.status} ${response.statusText}`);
|
|
112
|
+
}
|
|
113
|
+
return response.json();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
logger.error("Microsoft Graph API request failed:", error);
|
|
116
|
+
throw error;
|
|
120
117
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
118
|
+
}
|
|
119
|
+
async refreshAccessToken(refreshToken) {
|
|
120
|
+
const tenantId = process.env.MS365_MCP_TENANT_ID || "common";
|
|
121
|
+
const clientId = process.env.MS365_MCP_CLIENT_ID || "084a3e9f-a9f4-43f7-89f9-d229cf97853e";
|
|
122
|
+
const clientSecret = process.env.MS365_MCP_CLIENT_SECRET;
|
|
123
|
+
if (!clientSecret) {
|
|
124
|
+
throw new Error("MS365_MCP_CLIENT_SECRET not configured");
|
|
125
|
+
}
|
|
126
|
+
const response = await refreshAccessToken(refreshToken, clientId, clientSecret, tenantId);
|
|
127
|
+
this.accessToken = response.access_token;
|
|
128
|
+
if (response.refresh_token) {
|
|
129
|
+
this.refreshToken = response.refresh_token;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async performRequest(endpoint, accessToken, options) {
|
|
133
|
+
let url;
|
|
134
|
+
let sessionId = null;
|
|
135
|
+
if (options.excelFile && !endpoint.startsWith("/drive") && !endpoint.startsWith("/users") && !endpoint.startsWith("/me") && !endpoint.startsWith("/teams") && !endpoint.startsWith("/chats") && !endpoint.startsWith("/planner")) {
|
|
136
|
+
sessionId = await this.getSessionForFile(options.excelFile);
|
|
137
|
+
if (!sessionId) {
|
|
138
|
+
sessionId = await this.createSessionWithToken(options.excelFile, accessToken);
|
|
139
|
+
}
|
|
140
|
+
url = `https://graph.microsoft.com/v1.0/me/drive/root:${options.excelFile}:${endpoint}`;
|
|
141
|
+
} else if (endpoint.startsWith("/drive") || endpoint.startsWith("/users") || endpoint.startsWith("/me") || endpoint.startsWith("/teams") || endpoint.startsWith("/chats") || endpoint.startsWith("/planner")) {
|
|
142
|
+
url = `https://graph.microsoft.com/v1.0${endpoint}`;
|
|
143
|
+
} else {
|
|
144
|
+
throw new Error("Excel operation requested without specifying a file");
|
|
145
|
+
}
|
|
146
|
+
const headers = {
|
|
147
|
+
Authorization: `Bearer ${accessToken}`,
|
|
148
|
+
"Content-Type": "application/json",
|
|
149
|
+
...sessionId && { "workbook-session-id": sessionId },
|
|
150
|
+
...options.headers
|
|
151
|
+
};
|
|
152
|
+
return fetch(url, {
|
|
153
|
+
method: options.method || "GET",
|
|
154
|
+
headers,
|
|
155
|
+
body: options.body
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
async graphRequest(endpoint, options = {}) {
|
|
159
|
+
try {
|
|
160
|
+
logger.info(`Calling ${endpoint} with options: ${JSON.stringify(options)}`);
|
|
161
|
+
const result = await this.makeRequest(endpoint, options);
|
|
162
|
+
return this.formatJsonResponse(result, options.rawResponse);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
logger.error(`Error in Graph API request: ${error}`);
|
|
165
|
+
return {
|
|
166
|
+
content: [{ type: "text", text: JSON.stringify({ error: error.message }) }],
|
|
167
|
+
isError: true
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async createSessionWithToken(filePath, accessToken) {
|
|
172
|
+
try {
|
|
173
|
+
if (!filePath) {
|
|
174
|
+
logger.error("No file path provided for Excel session");
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const existingSession = await this.getSessionForFile(filePath);
|
|
178
|
+
if (existingSession) {
|
|
179
|
+
return existingSession;
|
|
180
|
+
}
|
|
181
|
+
logger.info(`Creating new Excel session for file: ${filePath}`);
|
|
182
|
+
const response = await fetch(
|
|
183
|
+
`https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/createSession`,
|
|
184
|
+
{
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: {
|
|
187
|
+
Authorization: `Bearer ${accessToken}`,
|
|
188
|
+
"Content-Type": "application/json"
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify({ persistChanges: true })
|
|
132
191
|
}
|
|
192
|
+
);
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
const errorText = await response.text();
|
|
195
|
+
logger.error(`Failed to create session: ${response.status} - ${errorText}`);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const result = await response.json();
|
|
199
|
+
logger.info(`Session created successfully for file: ${filePath}`);
|
|
200
|
+
await this.setSessionForFile(filePath, result.id);
|
|
201
|
+
return result.id;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
logger.error(`Error creating Excel session: ${error}`);
|
|
204
|
+
return null;
|
|
133
205
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
206
|
+
}
|
|
207
|
+
formatJsonResponse(data, rawResponse = false) {
|
|
208
|
+
if (rawResponse) {
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: "text", text: JSON.stringify(data) }]
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (data === null || data === void 0) {
|
|
214
|
+
return {
|
|
215
|
+
content: [{ type: "text", text: JSON.stringify({ success: true }) }]
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const removeODataProps = (obj) => {
|
|
219
|
+
if (typeof obj === "object" && obj !== null) {
|
|
220
|
+
Object.keys(obj).forEach((key) => {
|
|
221
|
+
if (key.startsWith("@odata.")) {
|
|
222
|
+
delete obj[key];
|
|
223
|
+
} else if (typeof obj[key] === "object") {
|
|
224
|
+
removeODataProps(obj[key]);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
removeODataProps(data);
|
|
230
|
+
return {
|
|
231
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async graphRequestOld(endpoint, options = {}) {
|
|
235
|
+
try {
|
|
236
|
+
logger.info(`Calling ${endpoint} with options: ${JSON.stringify(options)}`);
|
|
237
|
+
let accessToken = await this.authManager.getToken();
|
|
238
|
+
let url;
|
|
239
|
+
let sessionId = null;
|
|
240
|
+
if (options.excelFile && !endpoint.startsWith("/drive") && !endpoint.startsWith("/users") && !endpoint.startsWith("/me") && !endpoint.startsWith("/teams") && !endpoint.startsWith("/chats") && !endpoint.startsWith("/planner") && !endpoint.startsWith("/sites")) {
|
|
241
|
+
sessionId = await this.getSessionForFile(options.excelFile);
|
|
242
|
+
if (!sessionId) {
|
|
243
|
+
sessionId = await this.createSession(options.excelFile);
|
|
149
244
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
245
|
+
url = `https://graph.microsoft.com/v1.0/me/drive/root:${options.excelFile}:${endpoint}`;
|
|
246
|
+
} else if (endpoint.startsWith("/drive") || endpoint.startsWith("/users") || endpoint.startsWith("/me") || endpoint.startsWith("/teams") || endpoint.startsWith("/chats") || endpoint.startsWith("/planner") || endpoint.startsWith("/sites")) {
|
|
247
|
+
url = `https://graph.microsoft.com/v1.0${endpoint}`;
|
|
248
|
+
} else {
|
|
249
|
+
logger.error("Excel operation requested without specifying a file");
|
|
250
|
+
return {
|
|
251
|
+
content: [
|
|
252
|
+
{
|
|
253
|
+
type: "text",
|
|
254
|
+
text: JSON.stringify({ error: "No Excel file specified for this operation" })
|
|
255
|
+
}
|
|
256
|
+
]
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const headers = {
|
|
260
|
+
Authorization: `Bearer ${accessToken}`,
|
|
261
|
+
"Content-Type": "application/json",
|
|
262
|
+
...sessionId && { "workbook-session-id": sessionId },
|
|
263
|
+
...options.headers
|
|
264
|
+
};
|
|
265
|
+
delete options.headers;
|
|
266
|
+
logger.info(` ** Making request to ${url} with options: ${JSON.stringify(options)}`);
|
|
267
|
+
const response = await fetch(url, {
|
|
268
|
+
headers,
|
|
269
|
+
...options
|
|
270
|
+
});
|
|
271
|
+
if (response.status === 401) {
|
|
272
|
+
logger.info("Access token expired, refreshing...");
|
|
273
|
+
const newToken = await this.authManager.getToken(true);
|
|
274
|
+
if (options.excelFile && !endpoint.startsWith("/drive") && !endpoint.startsWith("/users") && !endpoint.startsWith("/me") && !endpoint.startsWith("/teams") && !endpoint.startsWith("/chats") && !endpoint.startsWith("/planner") && !endpoint.startsWith("/sites")) {
|
|
275
|
+
sessionId = await this.createSession(options.excelFile);
|
|
157
276
|
}
|
|
158
|
-
|
|
159
|
-
|
|
277
|
+
headers.Authorization = `Bearer ${newToken}`;
|
|
278
|
+
if (sessionId && !endpoint.startsWith("/drive") && !endpoint.startsWith("/users") && !endpoint.startsWith("/me") && !endpoint.startsWith("/teams") && !endpoint.startsWith("/chats") && !endpoint.startsWith("/planner") && !endpoint.startsWith("/sites")) {
|
|
279
|
+
headers["workbook-session-id"] = sessionId;
|
|
160
280
|
}
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
...(sessionId && { 'workbook-session-id': sessionId }),
|
|
165
|
-
...options.headers,
|
|
166
|
-
};
|
|
167
|
-
return fetch(url, {
|
|
168
|
-
method: options.method || 'GET',
|
|
169
|
-
headers,
|
|
170
|
-
body: options.body,
|
|
281
|
+
const retryResponse = await fetch(url, {
|
|
282
|
+
headers,
|
|
283
|
+
...options
|
|
171
284
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
logger.info(`Calling ${endpoint} with options: ${JSON.stringify(options)}`);
|
|
176
|
-
// Use new OAuth-aware request method
|
|
177
|
-
const result = await this.makeRequest(endpoint, options);
|
|
178
|
-
return this.formatJsonResponse(result, options.rawResponse);
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
logger.error(`Error in Graph API request: ${error}`);
|
|
182
|
-
return {
|
|
183
|
-
content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }],
|
|
184
|
-
isError: true,
|
|
185
|
-
};
|
|
285
|
+
if (!retryResponse.ok) {
|
|
286
|
+
throw new Error(`Graph API error: ${retryResponse.status} ${await retryResponse.text()}`);
|
|
186
287
|
}
|
|
288
|
+
return this.formatResponse(retryResponse, options.rawResponse);
|
|
289
|
+
}
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
throw new Error(`Graph API error: ${response.status} ${await response.text()}`);
|
|
292
|
+
}
|
|
293
|
+
return this.formatResponse(response, options.rawResponse);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
logger.error(`Error in Graph API request: ${error}`);
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: "text", text: JSON.stringify({ error: error.message }) }],
|
|
298
|
+
isError: true
|
|
299
|
+
};
|
|
187
300
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const response = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/createSession`, {
|
|
200
|
-
method: 'POST',
|
|
201
|
-
headers: {
|
|
202
|
-
Authorization: `Bearer ${accessToken}`,
|
|
203
|
-
'Content-Type': 'application/json',
|
|
204
|
-
},
|
|
205
|
-
body: JSON.stringify({ persistChanges: true }),
|
|
206
|
-
});
|
|
207
|
-
if (!response.ok) {
|
|
208
|
-
const errorText = await response.text();
|
|
209
|
-
logger.error(`Failed to create session: ${response.status} - ${errorText}`);
|
|
210
|
-
return null;
|
|
301
|
+
}
|
|
302
|
+
async formatResponse(response, rawResponse = false) {
|
|
303
|
+
try {
|
|
304
|
+
if (response.status === 204) {
|
|
305
|
+
return {
|
|
306
|
+
content: [
|
|
307
|
+
{
|
|
308
|
+
type: "text",
|
|
309
|
+
text: JSON.stringify({
|
|
310
|
+
message: "Operation completed successfully"
|
|
311
|
+
})
|
|
211
312
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
formatJsonResponse(data, rawResponse = false) {
|
|
223
|
-
if (rawResponse) {
|
|
224
|
-
return {
|
|
225
|
-
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
if (data === null || data === undefined) {
|
|
229
|
-
return {
|
|
230
|
-
content: [{ type: 'text', text: JSON.stringify({ success: true }) }],
|
|
231
|
-
};
|
|
313
|
+
]
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (rawResponse) {
|
|
317
|
+
const contentType2 = response.headers.get("content-type");
|
|
318
|
+
if (contentType2 && contentType2.startsWith("text/")) {
|
|
319
|
+
const text = await response.text();
|
|
320
|
+
return {
|
|
321
|
+
content: [{ type: "text", text }]
|
|
322
|
+
};
|
|
232
323
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
});
|
|
324
|
+
return {
|
|
325
|
+
content: [
|
|
326
|
+
{
|
|
327
|
+
type: "text",
|
|
328
|
+
text: JSON.stringify({
|
|
329
|
+
message: "Binary file content received",
|
|
330
|
+
contentType: contentType2,
|
|
331
|
+
contentLength: response.headers.get("content-length")
|
|
332
|
+
})
|
|
244
333
|
}
|
|
334
|
+
]
|
|
245
335
|
};
|
|
246
|
-
|
|
336
|
+
}
|
|
337
|
+
const contentType = response.headers.get("content-type");
|
|
338
|
+
if (contentType && !contentType.includes("application/json")) {
|
|
339
|
+
if (contentType.startsWith("text/")) {
|
|
340
|
+
const text = await response.text();
|
|
341
|
+
return {
|
|
342
|
+
content: [{ type: "text", text }]
|
|
343
|
+
};
|
|
344
|
+
}
|
|
247
345
|
return {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
let sessionId = null;
|
|
257
|
-
if (options.excelFile &&
|
|
258
|
-
!endpoint.startsWith('/drive') &&
|
|
259
|
-
!endpoint.startsWith('/users') &&
|
|
260
|
-
!endpoint.startsWith('/me') &&
|
|
261
|
-
!endpoint.startsWith('/teams') &&
|
|
262
|
-
!endpoint.startsWith('/chats') &&
|
|
263
|
-
!endpoint.startsWith('/planner') &&
|
|
264
|
-
!endpoint.startsWith('/sites')) {
|
|
265
|
-
sessionId = await this.getSessionForFile(options.excelFile);
|
|
266
|
-
if (!sessionId) {
|
|
267
|
-
sessionId = await this.createSession(options.excelFile);
|
|
268
|
-
}
|
|
269
|
-
url = `https://graph.microsoft.com/v1.0/me/drive/root:${options.excelFile}:${endpoint}`;
|
|
346
|
+
content: [
|
|
347
|
+
{
|
|
348
|
+
type: "text",
|
|
349
|
+
text: JSON.stringify({
|
|
350
|
+
message: "Binary or non-JSON content received",
|
|
351
|
+
contentType,
|
|
352
|
+
contentLength: response.headers.get("content-length")
|
|
353
|
+
})
|
|
270
354
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
type: 'text',
|
|
286
|
-
text: JSON.stringify({ error: 'No Excel file specified for this operation' }),
|
|
287
|
-
},
|
|
288
|
-
],
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
const headers = {
|
|
292
|
-
Authorization: `Bearer ${accessToken}`,
|
|
293
|
-
'Content-Type': 'application/json',
|
|
294
|
-
...(sessionId && { 'workbook-session-id': sessionId }),
|
|
295
|
-
...options.headers,
|
|
296
|
-
};
|
|
297
|
-
delete options.headers;
|
|
298
|
-
logger.info(` ** Making request to ${url} with options: ${JSON.stringify(options)}`);
|
|
299
|
-
const response = await fetch(url, {
|
|
300
|
-
headers,
|
|
301
|
-
...options,
|
|
302
|
-
});
|
|
303
|
-
if (response.status === 401) {
|
|
304
|
-
logger.info('Access token expired, refreshing...');
|
|
305
|
-
const newToken = await this.authManager.getToken(true);
|
|
306
|
-
if (options.excelFile &&
|
|
307
|
-
!endpoint.startsWith('/drive') &&
|
|
308
|
-
!endpoint.startsWith('/users') &&
|
|
309
|
-
!endpoint.startsWith('/me') &&
|
|
310
|
-
!endpoint.startsWith('/teams') &&
|
|
311
|
-
!endpoint.startsWith('/chats') &&
|
|
312
|
-
!endpoint.startsWith('/planner') &&
|
|
313
|
-
!endpoint.startsWith('/sites')) {
|
|
314
|
-
sessionId = await this.createSession(options.excelFile);
|
|
315
|
-
}
|
|
316
|
-
headers.Authorization = `Bearer ${newToken}`;
|
|
317
|
-
if (sessionId &&
|
|
318
|
-
!endpoint.startsWith('/drive') &&
|
|
319
|
-
!endpoint.startsWith('/users') &&
|
|
320
|
-
!endpoint.startsWith('/me') &&
|
|
321
|
-
!endpoint.startsWith('/teams') &&
|
|
322
|
-
!endpoint.startsWith('/chats') &&
|
|
323
|
-
!endpoint.startsWith('/planner') &&
|
|
324
|
-
!endpoint.startsWith('/sites')) {
|
|
325
|
-
headers['workbook-session-id'] = sessionId;
|
|
326
|
-
}
|
|
327
|
-
const retryResponse = await fetch(url, {
|
|
328
|
-
headers,
|
|
329
|
-
...options,
|
|
330
|
-
});
|
|
331
|
-
if (!retryResponse.ok) {
|
|
332
|
-
throw new Error(`Graph API error: ${retryResponse.status} ${await retryResponse.text()}`);
|
|
333
|
-
}
|
|
334
|
-
return this.formatResponse(retryResponse, options.rawResponse);
|
|
335
|
-
}
|
|
336
|
-
if (!response.ok) {
|
|
337
|
-
throw new Error(`Graph API error: ${response.status} ${await response.text()}`);
|
|
355
|
+
]
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const result = await response.json();
|
|
359
|
+
const removeODataProps = (obj) => {
|
|
360
|
+
if (!obj || typeof obj !== "object") return;
|
|
361
|
+
if (Array.isArray(obj)) {
|
|
362
|
+
obj.forEach((item) => removeODataProps(item));
|
|
363
|
+
} else {
|
|
364
|
+
Object.keys(obj).forEach((key) => {
|
|
365
|
+
if (key.startsWith("@odata") && !["@odata.nextLink", "@odata.count"].includes(key)) {
|
|
366
|
+
delete obj[key];
|
|
367
|
+
} else if (typeof obj[key] === "object") {
|
|
368
|
+
removeODataProps(obj[key]);
|
|
338
369
|
}
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
catch (error) {
|
|
342
|
-
logger.error(`Error in Graph API request: ${error}`);
|
|
343
|
-
return {
|
|
344
|
-
content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }],
|
|
345
|
-
isError: true,
|
|
346
|
-
};
|
|
370
|
+
});
|
|
347
371
|
}
|
|
372
|
+
};
|
|
373
|
+
removeODataProps(result);
|
|
374
|
+
return {
|
|
375
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
376
|
+
};
|
|
377
|
+
} catch (error) {
|
|
378
|
+
logger.error(`Error formatting response: ${error}`);
|
|
379
|
+
return {
|
|
380
|
+
content: [{ type: "text", text: JSON.stringify({ message: "Success" }) }]
|
|
381
|
+
};
|
|
348
382
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
if (rawResponse) {
|
|
364
|
-
const contentType = response.headers.get('content-type');
|
|
365
|
-
if (contentType && contentType.startsWith('text/')) {
|
|
366
|
-
const text = await response.text();
|
|
367
|
-
return {
|
|
368
|
-
content: [{ type: 'text', text }],
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
return {
|
|
372
|
-
content: [
|
|
373
|
-
{
|
|
374
|
-
type: 'text',
|
|
375
|
-
text: JSON.stringify({
|
|
376
|
-
message: 'Binary file content received',
|
|
377
|
-
contentType: contentType,
|
|
378
|
-
contentLength: response.headers.get('content-length'),
|
|
379
|
-
}),
|
|
380
|
-
},
|
|
381
|
-
],
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
const contentType = response.headers.get('content-type');
|
|
385
|
-
if (contentType && !contentType.includes('application/json')) {
|
|
386
|
-
if (contentType.startsWith('text/')) {
|
|
387
|
-
const text = await response.text();
|
|
388
|
-
return {
|
|
389
|
-
content: [{ type: 'text', text }],
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
return {
|
|
393
|
-
content: [
|
|
394
|
-
{
|
|
395
|
-
type: 'text',
|
|
396
|
-
text: JSON.stringify({
|
|
397
|
-
message: 'Binary or non-JSON content received',
|
|
398
|
-
contentType: contentType,
|
|
399
|
-
contentLength: response.headers.get('content-length'),
|
|
400
|
-
}),
|
|
401
|
-
},
|
|
402
|
-
],
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
const result = await response.json();
|
|
406
|
-
const removeODataProps = (obj) => {
|
|
407
|
-
if (!obj || typeof obj !== 'object')
|
|
408
|
-
return;
|
|
409
|
-
if (Array.isArray(obj)) {
|
|
410
|
-
obj.forEach((item) => removeODataProps(item));
|
|
411
|
-
}
|
|
412
|
-
else {
|
|
413
|
-
Object.keys(obj).forEach((key) => {
|
|
414
|
-
if (key.startsWith('@odata') && !['@odata.nextLink', '@odata.count'].includes(key)) {
|
|
415
|
-
delete obj[key];
|
|
416
|
-
}
|
|
417
|
-
else if (typeof obj[key] === 'object') {
|
|
418
|
-
removeODataProps(obj[key]);
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
};
|
|
423
|
-
removeODataProps(result);
|
|
424
|
-
return {
|
|
425
|
-
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
catch (error) {
|
|
429
|
-
logger.error(`Error formatting response: ${error}`);
|
|
430
|
-
return {
|
|
431
|
-
content: [{ type: 'text', text: JSON.stringify({ message: 'Success' }) }],
|
|
432
|
-
};
|
|
433
|
-
}
|
|
383
|
+
}
|
|
384
|
+
async closeSession(filePath) {
|
|
385
|
+
const sessionId = await this.getSessionForFile(filePath);
|
|
386
|
+
if (!filePath || !sessionId) {
|
|
387
|
+
return {
|
|
388
|
+
content: [
|
|
389
|
+
{
|
|
390
|
+
type: "text",
|
|
391
|
+
text: JSON.stringify({ message: "No active session for the specified file" })
|
|
392
|
+
}
|
|
393
|
+
]
|
|
394
|
+
};
|
|
434
395
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
try {
|
|
448
|
-
const accessToken = await this.authManager.getToken();
|
|
449
|
-
const response = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/closeSession`, {
|
|
450
|
-
method: 'POST',
|
|
451
|
-
headers: {
|
|
452
|
-
Authorization: `Bearer ${accessToken}`,
|
|
453
|
-
'Content-Type': 'application/json',
|
|
454
|
-
'workbook-session-id': sessionId,
|
|
455
|
-
},
|
|
456
|
-
});
|
|
457
|
-
if (response.ok) {
|
|
458
|
-
const accountId = await this.getCurrentAccountId();
|
|
459
|
-
if (accountId) {
|
|
460
|
-
const accountSessions = this.getAccountSessions(accountId);
|
|
461
|
-
accountSessions.delete(filePath);
|
|
462
|
-
}
|
|
463
|
-
return {
|
|
464
|
-
content: [
|
|
465
|
-
{
|
|
466
|
-
type: 'text',
|
|
467
|
-
text: JSON.stringify({ message: `Session for ${filePath} closed successfully` }),
|
|
468
|
-
},
|
|
469
|
-
],
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
else {
|
|
473
|
-
throw new Error(`Failed to close session: ${response.status}`);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
catch (error) {
|
|
477
|
-
logger.error(`Error closing session: ${error}`);
|
|
478
|
-
return {
|
|
479
|
-
content: [
|
|
480
|
-
{
|
|
481
|
-
type: 'text',
|
|
482
|
-
text: JSON.stringify({ error: `Failed to close session for ${filePath}` }),
|
|
483
|
-
},
|
|
484
|
-
],
|
|
485
|
-
isError: true,
|
|
486
|
-
};
|
|
396
|
+
try {
|
|
397
|
+
const accessToken = await this.authManager.getToken();
|
|
398
|
+
const response = await fetch(
|
|
399
|
+
`https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/closeSession`,
|
|
400
|
+
{
|
|
401
|
+
method: "POST",
|
|
402
|
+
headers: {
|
|
403
|
+
Authorization: `Bearer ${accessToken}`,
|
|
404
|
+
"Content-Type": "application/json",
|
|
405
|
+
"workbook-session-id": sessionId
|
|
406
|
+
}
|
|
487
407
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const results = [];
|
|
408
|
+
);
|
|
409
|
+
if (response.ok) {
|
|
491
410
|
const accountId = await this.getCurrentAccountId();
|
|
492
411
|
if (accountId) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const result = await this.closeSession(filePath);
|
|
496
|
-
results.push(result);
|
|
497
|
-
}
|
|
412
|
+
const accountSessions = this.getAccountSessions(accountId);
|
|
413
|
+
accountSessions.delete(filePath);
|
|
498
414
|
}
|
|
499
415
|
return {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
416
|
+
content: [
|
|
417
|
+
{
|
|
418
|
+
type: "text",
|
|
419
|
+
text: JSON.stringify({ message: `Session for ${filePath} closed successfully` })
|
|
420
|
+
}
|
|
421
|
+
]
|
|
506
422
|
};
|
|
423
|
+
} else {
|
|
424
|
+
throw new Error(`Failed to close session: ${response.status}`);
|
|
425
|
+
}
|
|
426
|
+
} catch (error) {
|
|
427
|
+
logger.error(`Error closing session: ${error}`);
|
|
428
|
+
return {
|
|
429
|
+
content: [
|
|
430
|
+
{
|
|
431
|
+
type: "text",
|
|
432
|
+
text: JSON.stringify({ error: `Failed to close session for ${filePath}` })
|
|
433
|
+
}
|
|
434
|
+
],
|
|
435
|
+
isError: true
|
|
436
|
+
};
|
|
507
437
|
}
|
|
438
|
+
}
|
|
439
|
+
async closeAllSessions() {
|
|
440
|
+
const results = [];
|
|
441
|
+
const accountId = await this.getCurrentAccountId();
|
|
442
|
+
if (accountId) {
|
|
443
|
+
const accountSessions = this.getAccountSessions(accountId);
|
|
444
|
+
for (const [filePath] of accountSessions) {
|
|
445
|
+
const result = await this.closeSession(filePath);
|
|
446
|
+
results.push(result);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
content: [
|
|
451
|
+
{
|
|
452
|
+
type: "text",
|
|
453
|
+
text: JSON.stringify({ message: "All sessions closed", results })
|
|
454
|
+
}
|
|
455
|
+
]
|
|
456
|
+
};
|
|
457
|
+
}
|
|
508
458
|
}
|
|
509
|
-
|
|
459
|
+
var graph_client_default = GraphClient;
|
|
460
|
+
export {
|
|
461
|
+
graph_client_default as default
|
|
462
|
+
};
|