@softeria/ms-365-mcp-server 0.1.11 → 0.2.2
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 +96 -17
- package/bin/release.mjs +39 -2
- package/index.mjs +17 -504
- package/package.json +1 -1
- package/src/auth-tools.mjs +74 -0
- package/{auth.mjs → src/auth.mjs} +5 -4
- package/src/calendar-tools.mjs +814 -0
- package/{cli.mjs → src/cli.mjs} +7 -2
- package/src/excel-tools.mjs +317 -0
- package/src/files-tools.mjs +554 -0
- package/src/graph-client.mjs +293 -0
- package/{logger.mjs → src/logger.mjs} +2 -3
- package/src/mail-tools.mjs +159 -0
- package/src/server.mjs +45 -0
- package/src/version.mjs +9 -0
- package/test/cli.test.js +3 -4
- package/test/graph-api.test.js +2 -1
- package/test/mcp-server.test.js +2 -3
- package/test/integration.test.js +0 -40
- package/test/simple.test.js +0 -7
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import logger from './logger.mjs';
|
|
2
|
+
|
|
3
|
+
class GraphClient {
|
|
4
|
+
constructor(authManager) {
|
|
5
|
+
this.authManager = authManager;
|
|
6
|
+
this.sessions = new Map();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async createSession(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
if (!filePath) {
|
|
12
|
+
logger.error('No file path provided for Excel session');
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (this.sessions.has(filePath)) {
|
|
17
|
+
return this.sessions.get(filePath);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
logger.info(`Creating new Excel session for file: ${filePath}`);
|
|
21
|
+
const accessToken = await this.authManager.getToken();
|
|
22
|
+
|
|
23
|
+
const response = await fetch(
|
|
24
|
+
`https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/createSession`,
|
|
25
|
+
{
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Bearer ${accessToken}`,
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({ persistChanges: true }),
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const errorText = await response.text();
|
|
37
|
+
logger.error(`Failed to create session: ${response.status} - ${errorText}`);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = await response.json();
|
|
42
|
+
logger.info(`Session created successfully for file: ${filePath}`);
|
|
43
|
+
|
|
44
|
+
this.sessions.set(filePath, result.id);
|
|
45
|
+
return result.id;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
logger.error(`Error creating Excel session: ${error}`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async graphRequest(endpoint, options = {}) {
|
|
53
|
+
try {
|
|
54
|
+
let accessToken = await this.authManager.getToken();
|
|
55
|
+
|
|
56
|
+
let url;
|
|
57
|
+
let sessionId = null;
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
options.excelFile &&
|
|
61
|
+
!endpoint.startsWith('/drive') &&
|
|
62
|
+
!endpoint.startsWith('/users') &&
|
|
63
|
+
!endpoint.startsWith('/me')
|
|
64
|
+
) {
|
|
65
|
+
sessionId = this.sessions.get(options.excelFile);
|
|
66
|
+
|
|
67
|
+
if (!sessionId) {
|
|
68
|
+
sessionId = await this.createSession(options.excelFile);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
url = `https://graph.microsoft.com/v1.0/me/drive/root:${options.excelFile}:${endpoint}`;
|
|
72
|
+
} else if (
|
|
73
|
+
endpoint.startsWith('/drive') ||
|
|
74
|
+
endpoint.startsWith('/users') ||
|
|
75
|
+
endpoint.startsWith('/me')
|
|
76
|
+
) {
|
|
77
|
+
url = `https://graph.microsoft.com/v1.0${endpoint}`;
|
|
78
|
+
} else {
|
|
79
|
+
logger.error('Excel operation requested without specifying a file');
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: JSON.stringify({ error: 'No Excel file specified for this operation' }),
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const headers = {
|
|
91
|
+
Authorization: `Bearer ${accessToken}`,
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
...(sessionId && { 'workbook-session-id': sessionId }),
|
|
94
|
+
...options.headers,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const response = await fetch(url, {
|
|
98
|
+
headers,
|
|
99
|
+
...options,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (response.status === 401) {
|
|
103
|
+
logger.info('Access token expired, refreshing...');
|
|
104
|
+
const newToken = await this.authManager.getToken(true);
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
options.excelFile &&
|
|
108
|
+
!endpoint.startsWith('/drive') &&
|
|
109
|
+
!endpoint.startsWith('/users') &&
|
|
110
|
+
!endpoint.startsWith('/me')
|
|
111
|
+
) {
|
|
112
|
+
sessionId = await this.createSession(options.excelFile);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
headers.Authorization = `Bearer ${newToken}`;
|
|
116
|
+
if (
|
|
117
|
+
sessionId &&
|
|
118
|
+
!endpoint.startsWith('/drive') &&
|
|
119
|
+
!endpoint.startsWith('/users') &&
|
|
120
|
+
!endpoint.startsWith('/me')
|
|
121
|
+
) {
|
|
122
|
+
headers['workbook-session-id'] = sessionId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const retryResponse = await fetch(url, {
|
|
126
|
+
headers,
|
|
127
|
+
...options,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!retryResponse.ok) {
|
|
131
|
+
throw new Error(`Graph API error: ${retryResponse.status} ${await retryResponse.text()}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return this.formatResponse(retryResponse, options.rawResponse);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
throw new Error(`Graph API error: ${response.status} ${await response.text()}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return this.formatResponse(response, options.rawResponse);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
logger.error(`Error in Graph API request: ${error}`);
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async formatResponse(response, rawResponse = false) {
|
|
151
|
+
try {
|
|
152
|
+
if (response.status === 204) {
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: 'text',
|
|
157
|
+
text: JSON.stringify({
|
|
158
|
+
message: 'Operation completed successfully',
|
|
159
|
+
}),
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (rawResponse) {
|
|
166
|
+
const contentType = response.headers.get('content-type');
|
|
167
|
+
|
|
168
|
+
if (contentType && contentType.startsWith('text/')) {
|
|
169
|
+
const text = await response.text();
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: 'text', text }],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
content: [
|
|
177
|
+
{
|
|
178
|
+
type: 'text',
|
|
179
|
+
text: JSON.stringify({
|
|
180
|
+
message: 'Binary file content received',
|
|
181
|
+
contentType: contentType,
|
|
182
|
+
contentLength: response.headers.get('content-length'),
|
|
183
|
+
}),
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const result = await response.json();
|
|
190
|
+
|
|
191
|
+
const removeODataProps = (obj) => {
|
|
192
|
+
if (!obj || typeof obj !== 'object') return;
|
|
193
|
+
|
|
194
|
+
if (Array.isArray(obj)) {
|
|
195
|
+
obj.forEach((item) => removeODataProps(item));
|
|
196
|
+
} else {
|
|
197
|
+
Object.keys(obj).forEach((key) => {
|
|
198
|
+
if (key.startsWith('@odata')) {
|
|
199
|
+
delete obj[key];
|
|
200
|
+
} else if (typeof obj[key] === 'object') {
|
|
201
|
+
removeODataProps(obj[key]);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
removeODataProps(result);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
logger.error(`Error formatting response: ${error}`);
|
|
214
|
+
return {
|
|
215
|
+
content: [{ type: 'text', text: JSON.stringify({ message: 'Success' }) }],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async closeSession(filePath) {
|
|
221
|
+
if (!filePath || !this.sessions.has(filePath)) {
|
|
222
|
+
return {
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: 'text',
|
|
226
|
+
text: JSON.stringify({ message: 'No active session for the specified file' }),
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const sessionId = this.sessions.get(filePath);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const accessToken = await this.authManager.getToken();
|
|
236
|
+
const response = await fetch(
|
|
237
|
+
`https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/closeSession`,
|
|
238
|
+
{
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: {
|
|
241
|
+
Authorization: `Bearer ${accessToken}`,
|
|
242
|
+
'Content-Type': 'application/json',
|
|
243
|
+
'workbook-session-id': sessionId,
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (response.ok) {
|
|
249
|
+
this.sessions.delete(filePath);
|
|
250
|
+
return {
|
|
251
|
+
content: [
|
|
252
|
+
{
|
|
253
|
+
type: 'text',
|
|
254
|
+
text: JSON.stringify({ message: `Session for ${filePath} closed successfully` }),
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
} else {
|
|
259
|
+
throw new Error(`Failed to close session: ${response.status}`);
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
logger.error(`Error closing session: ${error}`);
|
|
263
|
+
return {
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: 'text',
|
|
267
|
+
text: JSON.stringify({ error: `Failed to close session for ${filePath}` }),
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async closeAllSessions() {
|
|
275
|
+
const results = [];
|
|
276
|
+
|
|
277
|
+
for (const [filePath, _] of this.sessions) {
|
|
278
|
+
const result = await this.closeSession(filePath);
|
|
279
|
+
results.push(result);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
content: [
|
|
284
|
+
{
|
|
285
|
+
type: 'text',
|
|
286
|
+
text: JSON.stringify({ message: 'All sessions closed', results }),
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export default GraphClient;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import winston from 'winston';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
+
import fs from 'fs';
|
|
4
5
|
|
|
5
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
|
|
7
|
-
const logsDir = path.join(__dirname, 'logs');
|
|
8
|
-
import fs from 'fs';
|
|
7
|
+
const logsDir = path.join(__dirname, '..', 'logs');
|
|
9
8
|
|
|
10
9
|
if (!fs.existsSync(logsDir)) {
|
|
11
10
|
fs.mkdirSync(logsDir);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export function registerMailTools(server, graphClient) {
|
|
4
|
+
server.tool(
|
|
5
|
+
'get-message',
|
|
6
|
+
{
|
|
7
|
+
messageId: z.string().describe('ID of the message to retrieve'),
|
|
8
|
+
select: z.array(z.string()).optional().describe('Properties to include in the response'),
|
|
9
|
+
expandAttachments: z
|
|
10
|
+
.boolean()
|
|
11
|
+
.optional()
|
|
12
|
+
.default(false)
|
|
13
|
+
.describe('Whether to include attachment details'),
|
|
14
|
+
expandMentions: z
|
|
15
|
+
.boolean()
|
|
16
|
+
.optional()
|
|
17
|
+
.default(false)
|
|
18
|
+
.describe('Whether to include @mention details'),
|
|
19
|
+
expandSingleValueExtendedProperties: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.optional()
|
|
22
|
+
.default(false)
|
|
23
|
+
.describe('Whether to include single value extended properties'),
|
|
24
|
+
expandMultiValueExtendedProperties: z
|
|
25
|
+
.boolean()
|
|
26
|
+
.optional()
|
|
27
|
+
.default(false)
|
|
28
|
+
.describe('Whether to include multi-value extended properties'),
|
|
29
|
+
},
|
|
30
|
+
async ({
|
|
31
|
+
messageId,
|
|
32
|
+
select,
|
|
33
|
+
expandAttachments,
|
|
34
|
+
expandMentions,
|
|
35
|
+
expandSingleValueExtendedProperties,
|
|
36
|
+
expandMultiValueExtendedProperties,
|
|
37
|
+
}) => {
|
|
38
|
+
let endpoint = `/me/messages/${messageId}`;
|
|
39
|
+
|
|
40
|
+
const queryParams = [];
|
|
41
|
+
|
|
42
|
+
if (select && select.length > 0) {
|
|
43
|
+
queryParams.push(`$select=${select.join(',')}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const expandParams = [];
|
|
47
|
+
|
|
48
|
+
if (expandAttachments) {
|
|
49
|
+
expandParams.push('attachments');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (expandMentions) {
|
|
53
|
+
expandParams.push('mentions');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (expandSingleValueExtendedProperties) {
|
|
57
|
+
expandParams.push('singleValueExtendedProperties');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (expandMultiValueExtendedProperties) {
|
|
61
|
+
expandParams.push('multiValueExtendedProperties');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (expandParams.length > 0) {
|
|
65
|
+
queryParams.push(`$expand=${expandParams.join(',')}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (queryParams.length > 0) {
|
|
69
|
+
endpoint += '?' + queryParams.join('&');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return graphClient.graphRequest(endpoint, {
|
|
73
|
+
method: 'GET',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
server.tool(
|
|
79
|
+
'list-messages',
|
|
80
|
+
{
|
|
81
|
+
folderName: z
|
|
82
|
+
.string()
|
|
83
|
+
.optional()
|
|
84
|
+
.describe('Name of the folder to get messages from (e.g., "inbox", "drafts", "sentItems")'),
|
|
85
|
+
folderId: z
|
|
86
|
+
.string()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('ID of the folder to get messages from (instead of folderName)'),
|
|
89
|
+
filter: z.string().optional().describe('OData filter query (e.g., "isRead eq false")'),
|
|
90
|
+
select: z.array(z.string()).optional().describe('Properties to include in the response'),
|
|
91
|
+
top: z.number().optional().describe('Maximum number of messages to return'),
|
|
92
|
+
skip: z.number().optional().describe('Number of messages to skip'),
|
|
93
|
+
count: z
|
|
94
|
+
.boolean()
|
|
95
|
+
.optional()
|
|
96
|
+
.default(false)
|
|
97
|
+
.describe('Whether to include a count of the total number of messages'),
|
|
98
|
+
orderBy: z
|
|
99
|
+
.string()
|
|
100
|
+
.optional()
|
|
101
|
+
.describe('Property to sort by (e.g., "receivedDateTime desc")'),
|
|
102
|
+
search: z.string().optional().describe('Text to search for in messages'),
|
|
103
|
+
},
|
|
104
|
+
async ({ folderName, folderId, filter, select, top, skip, count, orderBy, search }) => {
|
|
105
|
+
let endpoint;
|
|
106
|
+
|
|
107
|
+
if (folderId) {
|
|
108
|
+
endpoint = `/me/mailFolders/${folderId}/messages`;
|
|
109
|
+
} else if (folderName) {
|
|
110
|
+
const standardFolderName = folderName.toLowerCase();
|
|
111
|
+
endpoint = `/me/mailFolders/${standardFolderName}/messages`;
|
|
112
|
+
} else {
|
|
113
|
+
endpoint = '/me/messages';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const queryParams = [];
|
|
117
|
+
|
|
118
|
+
if (filter) {
|
|
119
|
+
queryParams.push(`$filter=${encodeURIComponent(filter)}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (select && select.length > 0) {
|
|
123
|
+
queryParams.push(`$select=${select.join(',')}`);
|
|
124
|
+
} else {
|
|
125
|
+
queryParams.push(
|
|
126
|
+
'$select=id,subject,receivedDateTime,from,isRead,importance,hasAttachments'
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (top) {
|
|
131
|
+
queryParams.push(`$top=${top}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (skip) {
|
|
135
|
+
queryParams.push(`$skip=${skip}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (count) {
|
|
139
|
+
queryParams.push('$count=true');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (orderBy) {
|
|
143
|
+
queryParams.push(`$orderBy=${encodeURIComponent(orderBy)}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (search) {
|
|
147
|
+
queryParams.push(`$search="${encodeURIComponent(search)}"`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (queryParams.length > 0) {
|
|
151
|
+
endpoint += '?' + queryParams.join('&');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return graphClient.graphRequest(endpoint, {
|
|
155
|
+
method: 'GET',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
}
|
package/src/server.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import logger, { enableConsoleLogging } from './logger.mjs';
|
|
4
|
+
import { registerExcelTools } from './excel-tools.mjs';
|
|
5
|
+
import { registerAuthTools } from './auth-tools.mjs';
|
|
6
|
+
import { registerFilesTools } from './files-tools.mjs';
|
|
7
|
+
import { registerCalendarTools } from './calendar-tools.mjs';
|
|
8
|
+
import { registerMailTools } from './mail-tools.mjs';
|
|
9
|
+
import GraphClient from './graph-client.mjs';
|
|
10
|
+
|
|
11
|
+
class MicrosoftGraphServer {
|
|
12
|
+
constructor(authManager, options = {}) {
|
|
13
|
+
this.authManager = authManager;
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.graphClient = new GraphClient(authManager);
|
|
16
|
+
this.server = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async initialize(version) {
|
|
20
|
+
this.server = new McpServer({
|
|
21
|
+
name: 'Microsoft365MCP',
|
|
22
|
+
version,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
registerAuthTools(this.server, this.authManager);
|
|
26
|
+
registerFilesTools(this.server, this.graphClient);
|
|
27
|
+
registerExcelTools(this.server, this.graphClient);
|
|
28
|
+
registerCalendarTools(this.server, this.graphClient);
|
|
29
|
+
registerMailTools(this.server, this.graphClient);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async start() {
|
|
33
|
+
if (this.options.v) {
|
|
34
|
+
enableConsoleLogging();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
logger.info('Microsoft 365 MCP Server starting...');
|
|
38
|
+
|
|
39
|
+
const transport = new StdioServerTransport();
|
|
40
|
+
await this.server.connect(transport);
|
|
41
|
+
logger.info('Server connected to transport');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default MicrosoftGraphServer;
|
package/src/version.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
7
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
8
|
+
|
|
9
|
+
export const version = packageJson.version;
|
package/test/cli.test.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { parseArgs } from '../src/cli.mjs';
|
|
3
|
+
|
|
2
4
|
vi.mock('commander', () => {
|
|
3
5
|
const mockCommand = {
|
|
4
6
|
name: vi.fn().mockReturnThis(),
|
|
@@ -24,9 +26,6 @@ vi.mock('../auth.mjs', () => {
|
|
|
24
26
|
});
|
|
25
27
|
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
26
28
|
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
|
27
|
-
import { Command } from 'commander';
|
|
28
|
-
import AuthManager from '../auth.mjs';
|
|
29
|
-
import { parseArgs } from '../cli.mjs';
|
|
30
29
|
|
|
31
30
|
describe('CLI Module', () => {
|
|
32
31
|
beforeEach(() => {
|
package/test/graph-api.test.js
CHANGED
package/test/mcp-server.test.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
4
|
|
|
4
5
|
vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
|
|
5
6
|
McpServer: vi.fn(() => ({
|
|
@@ -11,8 +12,6 @@ vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
|
|
|
11
12
|
}));
|
|
12
13
|
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
13
14
|
|
|
14
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
|
-
|
|
16
15
|
describe('MCP Server', () => {
|
|
17
16
|
let server;
|
|
18
17
|
|
package/test/integration.test.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
|
|
5
|
-
describe('Integration Tests', () => {
|
|
6
|
-
it('should have correct package.json configuration', () => {
|
|
7
|
-
const packagePath = path.resolve(process.cwd(), 'package.json');
|
|
8
|
-
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
9
|
-
|
|
10
|
-
expect(packageJson).toHaveProperty('type', 'module');
|
|
11
|
-
expect(packageJson).toHaveProperty('bin.ms-365-mcp-server');
|
|
12
|
-
expect(packageJson.bin['ms-365-mcp-server']).toEqual('index.mjs');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should have all required dependencies', () => {
|
|
16
|
-
const packagePath = path.resolve(process.cwd(), 'package.json');
|
|
17
|
-
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
18
|
-
|
|
19
|
-
const requiredDependencies = [
|
|
20
|
-
'@azure/msal-node',
|
|
21
|
-
'@modelcontextprotocol/sdk',
|
|
22
|
-
'commander',
|
|
23
|
-
'keytar',
|
|
24
|
-
'zod',
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
requiredDependencies.forEach((dep) => {
|
|
28
|
-
expect(packageJson.dependencies).toHaveProperty(dep);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should have all required files', () => {
|
|
33
|
-
const requiredFiles = ['index.mjs', 'auth.mjs', 'cli.mjs', 'package.json', 'README.md'];
|
|
34
|
-
|
|
35
|
-
requiredFiles.forEach((file) => {
|
|
36
|
-
const filePath = path.resolve(process.cwd(), file);
|
|
37
|
-
expect(fs.existsSync(filePath)).toBe(true);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
});
|