@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/index.mjs CHANGED
@@ -1,510 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
- import { z } from 'zod';
6
- import { parseArgs } from './cli.mjs';
7
- import { readFileSync } from 'fs';
8
- import logger, { enableConsoleLogging } from './logger.mjs';
9
- import AuthManager from './auth.mjs';
10
-
11
- const packageJson = JSON.parse(readFileSync(new URL('./package.json', import.meta.url)));
12
- export const version = packageJson.version;
13
-
14
- const args = parseArgs();
15
- const filePath = args.file || '/Livet.xlsx';
16
-
17
- const authManager = new AuthManager();
18
- await authManager.loadTokenCache();
19
-
20
- let sessionId = null;
21
-
22
- async function createSession() {
23
- try {
24
- logger.info('Creating new Excel session...');
25
- const accessToken = await authManager.getToken();
26
-
27
- const response = await fetch(
28
- `https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/createSession`,
29
- {
30
- method: 'POST',
31
- headers: {
32
- Authorization: `Bearer ${accessToken}`,
33
- 'Content-Type': 'application/json',
34
- },
35
- body: JSON.stringify({ persistChanges: true }),
36
- }
37
- );
38
-
39
- if (!response.ok) {
40
- const errorText = await response.text();
41
- logger.error(`Failed to create session: ${response.status} - ${errorText}`);
42
- return null;
43
- }
44
-
45
- const result = await response.json();
46
- logger.info('Session created successfully');
47
- sessionId = result.id;
48
- return sessionId;
49
- } catch (error) {
50
- logger.error(`Error creating Excel session: ${error}`);
51
- return null;
52
- }
53
- }
54
-
55
- async function graphRequest(endpoint, options = {}) {
56
- try {
57
- let accessToken = await authManager.getToken();
58
-
59
- const headers = {
60
- Authorization: `Bearer ${accessToken}`,
61
- 'Content-Type': 'application/json',
62
- ...(sessionId && { 'workbook-session-id': sessionId }),
63
- ...options.headers,
64
- };
65
-
66
- const response = await fetch(
67
- `https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:${endpoint}`,
68
- {
69
- headers,
70
- ...options,
71
- }
72
- );
73
-
74
- if (response.status === 401) {
75
- logger.info('Access token expired, refreshing...');
76
- const newToken = await authManager.getToken(true);
77
- await createSession();
78
-
79
- headers.Authorization = `Bearer ${newToken}`;
80
- if (sessionId) {
81
- headers['workbook-session-id'] = sessionId;
82
- }
83
-
84
- const retryResponse = await fetch(
85
- `https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:${endpoint}`,
86
- {
87
- headers,
88
- ...options,
89
- }
90
- );
91
-
92
- if (!retryResponse.ok) {
93
- throw new Error(`Graph API error: ${retryResponse.status} ${await retryResponse.text()}`);
94
- }
95
-
96
- return formatResponse(retryResponse);
97
- }
98
-
99
- if (!response.ok) {
100
- throw new Error(`Graph API error: ${response.status} ${await response.text()}`);
101
- }
102
-
103
- return formatResponse(response);
104
- } catch (error) {
105
- logger.error(`Error in Graph API request: ${error}`);
106
- return {
107
- content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }],
108
- };
109
- }
110
- }
111
-
112
- async function formatResponse(response) {
113
- try {
114
- if (response.status === 204) {
115
- return {
116
- content: [
117
- {
118
- type: 'text',
119
- text: JSON.stringify({
120
- message: 'Operation completed successfully',
121
- }),
122
- },
123
- ],
124
- };
125
- }
126
-
127
- const result = await response.json();
128
-
129
- const removeODataProps = (obj) => {
130
- if (!obj || typeof obj !== 'object') return;
131
-
132
- if (Array.isArray(obj)) {
133
- obj.forEach((item) => removeODataProps(item));
134
- } else {
135
- Object.keys(obj).forEach((key) => {
136
- if (key.startsWith('@odata')) {
137
- delete obj[key];
138
- } else if (typeof obj[key] === 'object') {
139
- removeODataProps(obj[key]);
140
- }
141
- });
142
- }
143
- };
144
-
145
- removeODataProps(result);
146
-
147
- return {
148
- content: [{ type: 'text', text: JSON.stringify(result) }],
149
- };
150
- } catch (error) {
151
- return {
152
- content: [{ type: 'text', text: JSON.stringify({ message: 'Success' }) }],
153
- };
154
- }
155
- }
156
-
157
- const server = new McpServer({
158
- name: 'ExcelUpdater',
159
- version,
160
- });
161
-
162
- server.tool('login', {}, async () => {
163
- try {
164
- const text = await new Promise((r) => {
165
- authManager.acquireTokenByDeviceCode(r);
166
- });
167
- return {
168
- content: [
169
- {
170
- type: 'text',
171
- text,
172
- },
173
- ],
174
- };
175
- } catch (error) {
176
- return {
177
- content: [
178
- {
179
- type: 'text',
180
- text: JSON.stringify({ error: `Authentication failed: ${error.message}` }),
181
- },
182
- ],
183
- };
184
- }
185
- });
186
-
187
- server.tool('logout', {}, async () => {
188
- try {
189
- await authManager.logout();
190
- return {
191
- content: [
192
- {
193
- type: 'text',
194
- text: JSON.stringify({ message: 'Logged out successfully' }),
195
- },
196
- ],
197
- };
198
- } catch (error) {
199
- return {
200
- content: [
201
- {
202
- type: 'text',
203
- text: JSON.stringify({ error: 'Logout failed' }),
204
- },
205
- ],
206
- };
207
- }
208
- });
209
-
210
- server.tool('test-login', {}, async () => {
211
- const result = await authManager.testLogin();
212
- return {
213
- content: [
214
- {
215
- type: 'text',
216
- text: JSON.stringify(result),
217
- },
218
- ],
219
- };
220
- });
221
-
222
- server.tool('verify-login', {}, async () => {
223
- // Test the login after the user has completed the device code authentication
224
- const testResult = await authManager.testLogin();
225
-
226
- return {
227
- content: [
228
- {
229
- type: 'text',
230
- text: JSON.stringify(testResult),
231
- },
232
- ],
233
- };
234
- });
235
-
236
- server.tool(
237
- 'update-excel',
238
- {
239
- worksheet: z.string().default('Sheet1').describe('Worksheet name'),
240
- address: z.string().describe("Range address (e.g., 'A1:B5')"),
241
- values: z.array(z.array(z.any())).describe('Values to update'),
242
- },
243
- async ({ worksheet, address, values }) => {
244
- return graphRequest(`/workbook/worksheets('${worksheet}')/range(address='${address}')`, {
245
- method: 'PATCH',
246
- body: JSON.stringify({ values }),
247
- });
248
- }
249
- );
250
-
251
- server.tool(
252
- 'create-chart',
253
- {
254
- worksheet: z.string().default('Sheet1').describe('Worksheet name'),
255
- type: z.string().describe("Chart type (e.g., 'ColumnClustered', 'Line', 'Pie')"),
256
- dataRange: z.string().describe("Data range for the chart (e.g., 'A1:B10')"),
257
- title: z.string().optional().describe('Title for the chart'),
258
- position: z
259
- .object({
260
- x: z.number().describe('X position'),
261
- y: z.number().describe('Y position'),
262
- width: z.number().describe('Width'),
263
- height: z.number().describe('Height'),
264
- })
265
- .describe('Chart position and dimensions'),
266
- },
267
- async ({ worksheet, type, dataRange, title, position }) => {
268
- const body = {
269
- type,
270
- sourceData: dataRange,
271
- position,
272
- };
273
-
274
- if (title) {
275
- body.title = { text: title };
276
- }
277
-
278
- return graphRequest(`/workbook/worksheets('${worksheet}')/charts/add`, {
279
- method: 'POST',
280
- body: JSON.stringify(body),
281
- });
282
- }
283
- );
284
-
285
- server.tool(
286
- 'format-range',
287
- {
288
- worksheet: z.string().default('Sheet1').describe('Worksheet name'),
289
- range: z.string().describe("Range address (e.g., 'A1:B5')"),
290
- format: z
291
- .object({
292
- fill: z
293
- .object({
294
- color: z.string().optional().describe("Background color (e.g., '#FFFF00')"),
295
- })
296
- .optional(),
297
- font: z
298
- .object({
299
- bold: z.boolean().optional().describe('Bold text'),
300
- italic: z.boolean().optional().describe('Italic text'),
301
- color: z.string().optional().describe("Font color (e.g., '#FF0000')"),
302
- size: z.number().optional().describe('Font size'),
303
- })
304
- .optional(),
305
- numberFormat: z.string().optional().describe("Number format (e.g., '0.00%', 'mm/dd/yyyy')"),
306
- })
307
- .describe('Formatting to apply'),
308
- },
309
- async ({ worksheet, range, format }) => {
310
- return graphRequest(`/workbook/worksheets('${worksheet}')/range(address='${range}')/format`, {
311
- method: 'PATCH',
312
- body: JSON.stringify(format),
313
- });
314
- }
315
- );
316
-
317
- server.tool(
318
- 'sort-range',
319
- {
320
- worksheet: z.string().default('Sheet1').describe('Worksheet name'),
321
- range: z.string().describe("Range address (e.g., 'A1:B5')"),
322
- sortFields: z
323
- .array(
324
- z.object({
325
- key: z.number().describe('Column index to sort by (zero-based)'),
326
- sortOn: z
327
- .string()
328
- .optional()
329
- .describe("Sorting criteria (e.g., 'Value', 'CellColor', 'FontColor', 'Icon')"),
330
- ascending: z.boolean().optional().describe('Sort in ascending order (default: true)'),
331
- color: z
332
- .object({
333
- color: z.string().describe('HTML color code'),
334
- type: z.string().describe("Color type (e.g., 'Background', 'Font')"),
335
- })
336
- .optional()
337
- .describe('Color information for sorting by color'),
338
- dataOption: z
339
- .string()
340
- .optional()
341
- .describe("Data option for sorting (e.g., 'Normal', 'TextAsNumber')"),
342
- icon: z
343
- .object({
344
- set: z.string().describe('Icon set name'),
345
- index: z.number().describe('Icon index'),
346
- })
347
- .optional()
348
- .describe('Icon information for sorting by icon'),
349
- })
350
- )
351
- .describe('Fields to sort by'),
352
- matchCase: z.boolean().optional().describe('Whether the sort is case-sensitive'),
353
- hasHeaders: z.boolean().optional().describe('Whether the range has headers (default: false)'),
354
- orientation: z.string().optional().describe("Sort orientation ('Rows' or 'Columns')"),
355
- method: z
356
- .string()
357
- .optional()
358
- .describe("Sort method for Chinese characters ('PinYin' or 'StrokeCount')"),
359
- },
360
- async ({ worksheet, range, sortFields, matchCase, hasHeaders, orientation, method }) => {
361
- const body = {
362
- fields: sortFields,
363
- };
364
-
365
- if (matchCase !== undefined) body.matchCase = matchCase;
366
- if (hasHeaders !== undefined) body.hasHeaders = hasHeaders;
367
- if (orientation) body.orientation = orientation;
368
- if (method) body.method = method;
369
-
370
- return graphRequest(
371
- `/workbook/worksheets('${worksheet}')/range(address='${range}')/sort/apply`,
372
- {
373
- method: 'POST',
374
- body: JSON.stringify(body),
375
- }
376
- );
377
- }
378
- );
379
-
380
- server.tool(
381
- 'create-table',
382
- {
383
- worksheet: z.string().default('Sheet1').describe('Worksheet name'),
384
- range: z.string().describe("Range address (e.g., 'A1:B5')"),
385
- hasHeaders: z.boolean().optional().describe('Whether the range has headers'),
386
- tableName: z.string().optional().describe('Name for the new table'),
387
- },
388
- async ({ worksheet, range, hasHeaders = true, tableName }) => {
389
- const body = {
390
- address: range,
391
- hasHeaders,
392
- };
393
-
394
- if (tableName) {
395
- body.name = tableName;
396
- }
397
-
398
- return graphRequest(`/workbook/worksheets('${worksheet}')/tables/add`, {
399
- method: 'POST',
400
- body: JSON.stringify(body),
401
- });
402
- }
403
- );
404
-
405
- server.tool(
406
- 'get-range',
407
- {
408
- worksheet: z.string().default('Sheet1').describe('Worksheet name'),
409
- range: z.string().describe("Range address (e.g., 'A1:B5')"),
410
- },
411
- async ({ worksheet, range }) => {
412
- return graphRequest(`/workbook/worksheets('${worksheet}')/range(address='${range}')`, {
413
- method: 'GET',
414
- });
415
- }
416
- );
417
-
418
- server.tool('list-worksheets', {}, async () => {
419
- return graphRequest('/workbook/worksheets', {
420
- method: 'GET',
421
- });
422
- });
423
-
424
- server.tool('close-session', {}, async () => {
425
- if (!sessionId) {
426
- return {
427
- content: [
428
- {
429
- type: 'text',
430
- text: JSON.stringify({ message: 'No active session' }),
431
- },
432
- ],
433
- };
434
- }
435
-
436
- try {
437
- const accessToken = await authManager.getToken();
438
- const response = await fetch(
439
- `https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/closeSession`,
440
- {
441
- method: 'POST',
442
- headers: {
443
- Authorization: `Bearer ${accessToken}`,
444
- 'Content-Type': 'application/json',
445
- 'workbook-session-id': sessionId,
446
- },
447
- }
448
- );
449
-
450
- if (response.ok) {
451
- sessionId = null;
452
- return {
453
- content: [
454
- {
455
- type: 'text',
456
- text: JSON.stringify({ message: 'Session closed successfully' }),
457
- },
458
- ],
459
- };
460
- } else {
461
- throw new Error(`Failed to close session: ${response.status}`);
462
- }
463
- } catch (error) {
464
- logger.error(`Error closing session: ${error}`);
465
- return {
466
- content: [
467
- {
468
- type: 'text',
469
- text: JSON.stringify({ error: 'Failed to close session' }),
470
- },
471
- ],
472
- };
473
- }
474
- });
475
-
476
- server.tool(
477
- 'delete-chart',
478
- {
479
- worksheet: z.string().default('Sheet1').describe('Worksheet name'),
480
- chartName: z.string().describe('The name of the chart to delete'),
481
- },
482
- async ({ worksheet, chartName }) => {
483
- return graphRequest(`/workbook/worksheets('${worksheet}')/charts('${chartName}')`, {
484
- method: 'DELETE',
485
- });
486
- }
487
- );
488
-
489
- server.tool(
490
- 'get-charts',
491
- {
492
- worksheet: z.string().default('Sheet1').describe('Worksheet name'),
493
- },
494
- async ({ worksheet }) => {
495
- return graphRequest(`/workbook/worksheets('${worksheet}')/charts`, {
496
- method: 'GET',
497
- });
498
- }
499
- );
3
+ import { parseArgs } from './src/cli.mjs';
4
+ import logger from './src/logger.mjs';
5
+ import AuthManager from './src/auth.mjs';
6
+ import MicrosoftGraphServer from './src/server.mjs';
7
+ import { version } from './src/version.mjs';
500
8
 
501
9
  async function main() {
502
10
  try {
503
- if (args.v) {
504
- enableConsoleLogging();
505
- }
11
+ const args = parseArgs();
506
12
 
507
- logger.info('Microsoft 365 MCP Server starting...');
13
+ const authManager = new AuthManager();
14
+ await authManager.loadTokenCache();
508
15
 
509
16
  if (args.login) {
510
17
  await authManager.acquireTokenByDeviceCode();
@@ -515,15 +22,21 @@ async function main() {
515
22
  }
516
23
 
517
24
  if (args.testLogin) {
25
+ logger.info('Testing login...');
518
26
  const result = await authManager.testLogin();
519
27
  console.log(JSON.stringify(result));
520
28
  process.exit(0);
521
29
  }
522
30
 
523
- await createSession();
31
+ if (args.logout) {
32
+ await authManager.logout();
33
+ console.log(JSON.stringify({ message: 'Logged out successfully' }));
34
+ process.exit(0);
35
+ }
524
36
 
525
- const transport = new StdioServerTransport();
526
- await server.connect(transport);
37
+ const server = new MicrosoftGraphServer(authManager, args);
38
+ await server.initialize(version);
39
+ await server.start();
527
40
  } catch (error) {
528
41
  logger.error(`Startup error: ${error}`);
529
42
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.1.11",
3
+ "version": "0.2.2",
4
4
  "description": "Microsoft 365 MCP Server",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
@@ -0,0 +1,74 @@
1
+ export function registerAuthTools(server, authManager) {
2
+ server.tool('login', {}, async () => {
3
+ try {
4
+ const text = await new Promise((r) => {
5
+ authManager.acquireTokenByDeviceCode(r);
6
+ });
7
+ return {
8
+ content: [
9
+ {
10
+ type: 'text',
11
+ text,
12
+ },
13
+ ],
14
+ };
15
+ } catch (error) {
16
+ return {
17
+ content: [
18
+ {
19
+ type: 'text',
20
+ text: JSON.stringify({ error: `Authentication failed: ${error.message}` }),
21
+ },
22
+ ],
23
+ };
24
+ }
25
+ });
26
+
27
+ server.tool('logout', {}, async () => {
28
+ try {
29
+ await authManager.logout();
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text',
34
+ text: JSON.stringify({ message: 'Logged out successfully' }),
35
+ },
36
+ ],
37
+ };
38
+ } catch (error) {
39
+ return {
40
+ content: [
41
+ {
42
+ type: 'text',
43
+ text: JSON.stringify({ error: 'Logout failed' }),
44
+ },
45
+ ],
46
+ };
47
+ }
48
+ });
49
+
50
+ server.tool('test-login', {}, async () => {
51
+ const result = await authManager.testLogin();
52
+ return {
53
+ content: [
54
+ {
55
+ type: 'text',
56
+ text: JSON.stringify(result),
57
+ },
58
+ ],
59
+ };
60
+ });
61
+
62
+ server.tool('verify-login', {}, async () => {
63
+ const testResult = await authManager.testLogin();
64
+
65
+ return {
66
+ content: [
67
+ {
68
+ type: 'text',
69
+ text: JSON.stringify(testResult),
70
+ },
71
+ ],
72
+ };
73
+ });
74
+ }
@@ -8,7 +8,7 @@ import logger from './logger.mjs';
8
8
  const SERVICE_NAME = 'ms-365-mcp-server';
9
9
  const TOKEN_CACHE_ACCOUNT = 'msal-token-cache';
10
10
  const FALLBACK_DIR = path.dirname(fileURLToPath(import.meta.url));
11
- const FALLBACK_PATH = path.join(FALLBACK_DIR, '.token-cache.json');
11
+ const FALLBACK_PATH = path.join(FALLBACK_DIR, '..', '.token-cache.json');
12
12
 
13
13
  const DEFAULT_CONFIG = {
14
14
  auth: {
@@ -19,10 +19,11 @@ const DEFAULT_CONFIG = {
19
19
 
20
20
  const DEFAULT_SCOPES = [
21
21
  'Files.ReadWrite',
22
- 'Files.ReadWrite.All',
23
- 'Sites.ReadWrite.All',
24
22
  'User.Read',
25
- 'User.ReadBasic.All',
23
+ 'Calendars.Read',
24
+ 'Calendars.ReadWrite',
25
+ 'Mail.Read',
26
+ 'Mail.ReadWrite',
26
27
  ];
27
28
 
28
29
  class AuthManager {