@softeria/ms-365-mcp-server 0.4.0 → 0.4.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 CHANGED
@@ -82,6 +82,31 @@ integration method.
82
82
 
83
83
  Tokens are cached securely in your OS credential store (fallback to file).
84
84
 
85
+ ## CLI Options
86
+
87
+ The following options can be used when running ms-365-mcp-server directly from the command line:
88
+
89
+ ```
90
+ --login Login using device code flow
91
+ --logout Log out and clear saved credentials
92
+ --verify-login Verify login without starting the server
93
+ ```
94
+
95
+ ### Server Options
96
+
97
+ When running as an MCP server, the following options can be used:
98
+
99
+ ```
100
+ -v Enable verbose logging
101
+ --read-only Start server in read-only mode, disabling write operations
102
+ ```
103
+
104
+ Environment variables:
105
+
106
+ - `READ_ONLY=true|1`: Alternative to --read-only flag
107
+ - `LOG_LEVEL`: Set logging level (default: 'info')
108
+ - `SILENT=true`: Disable console output
109
+
85
110
  ## License
86
111
 
87
112
  MIT © 2025 Softeria
package/dist/auth.js CHANGED
@@ -5,7 +5,7 @@ import path from 'path';
5
5
  import fs from 'fs';
6
6
  import logger from './logger.js';
7
7
  const endpoints = await import('./endpoints.json', {
8
- assert: { type: 'json' },
8
+ with: { type: 'json' },
9
9
  });
10
10
  const SERVICE_NAME = 'ms-365-mcp-server';
11
11
  const TOKEN_CACHE_ACCOUNT = 'msal-token-cache';
package/dist/cli.js CHANGED
@@ -14,8 +14,13 @@ program
14
14
  .option('-v', 'Enable verbose logging')
15
15
  .option('--login', 'Login using device code flow')
16
16
  .option('--logout', 'Log out and clear saved credentials')
17
- .option('--verify-login', 'Verify login without starting the server');
17
+ .option('--verify-login', 'Verify login without starting the server')
18
+ .option('--read-only', 'Start server in read-only mode, disabling write operations');
18
19
  export function parseArgs() {
19
20
  program.parse();
20
- return program.opts();
21
+ const options = program.opts();
22
+ if (process.env.READ_ONLY === 'true' || process.env.READ_ONLY === '1') {
23
+ options.readOnly = true;
24
+ }
25
+ return options;
21
26
  }
@@ -1,9 +1,12 @@
1
1
  import logger from './logger.js';
2
2
  import { api } from './generated/client.js';
3
3
  import { z } from 'zod';
4
- export function registerGraphTools(server, graphClient) {
4
+ export function registerGraphTools(server, graphClient, readOnly = false) {
5
5
  for (const tool of api.endpoints) {
6
- // Create a zod schema for the parameters
6
+ if (readOnly && tool.method.toUpperCase() !== 'GET') {
7
+ logger.info(`Skipping write operation ${tool.alias} in read-only mode`);
8
+ continue;
9
+ }
7
10
  const paramSchema = {};
8
11
  if (tool.parameters && tool.parameters.length > 0) {
9
12
  for (const param of tool.parameters) {
package/dist/server.js CHANGED
@@ -17,13 +17,16 @@ class MicrosoftGraphServer {
17
17
  version,
18
18
  });
19
19
  registerAuthTools(this.server, this.authManager);
20
- registerGraphTools(this.server, this.graphClient);
20
+ registerGraphTools(this.server, this.graphClient, this.options.readOnly);
21
21
  }
22
22
  async start() {
23
23
  if (this.options.v) {
24
24
  enableConsoleLogging();
25
25
  }
26
26
  logger.info('Microsoft 365 MCP Server starting...');
27
+ if (this.options.readOnly) {
28
+ logger.info('Server running in READ-ONLY mode. Write operations are disabled.');
29
+ }
27
30
  const transport = new StdioServerTransport();
28
31
  await this.server.connect(transport);
29
32
  logger.info('Server connected to transport');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Microsoft 365 MCP Server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/auth.ts CHANGED
@@ -7,7 +7,7 @@ import fs from 'fs';
7
7
  import logger from './logger.js';
8
8
 
9
9
  const endpoints = await import('./endpoints.json', {
10
- assert: { type: 'json' },
10
+ with: { type: 'json' },
11
11
  });
12
12
 
13
13
  const SERVICE_NAME = 'ms-365-mcp-server';
package/src/cli.ts CHANGED
@@ -17,17 +17,26 @@ program
17
17
  .option('-v', 'Enable verbose logging')
18
18
  .option('--login', 'Login using device code flow')
19
19
  .option('--logout', 'Log out and clear saved credentials')
20
- .option('--verify-login', 'Verify login without starting the server');
20
+ .option('--verify-login', 'Verify login without starting the server')
21
+ .option('--read-only', 'Start server in read-only mode, disabling write operations');
21
22
 
22
23
  export interface CommandOptions {
23
24
  v?: boolean;
24
25
  login?: boolean;
25
26
  logout?: boolean;
26
27
  verifyLogin?: boolean;
28
+ readOnly?: boolean;
29
+
27
30
  [key: string]: any;
28
31
  }
29
32
 
30
33
  export function parseArgs(): CommandOptions {
31
34
  program.parse();
32
- return program.opts();
35
+ const options = program.opts();
36
+
37
+ if (process.env.READ_ONLY === 'true' || process.env.READ_ONLY === '1') {
38
+ options.readOnly = true;
39
+ }
40
+
41
+ return options;
33
42
  }
@@ -58,9 +58,17 @@ interface CallToolResult {
58
58
  [key: string]: unknown;
59
59
  }
60
60
 
61
- export function registerGraphTools(server: McpServer, graphClient: GraphClient): void {
61
+ export function registerGraphTools(
62
+ server: McpServer,
63
+ graphClient: GraphClient,
64
+ readOnly: boolean = false
65
+ ): void {
62
66
  for (const tool of api.endpoints) {
63
- // Create a zod schema for the parameters
67
+ if (readOnly && tool.method.toUpperCase() !== 'GET') {
68
+ logger.info(`Skipping write operation ${tool.alias} in read-only mode`);
69
+ continue;
70
+ }
71
+
64
72
  const paramSchema: Record<string, any> = {};
65
73
  if (tool.parameters && tool.parameters.length > 0) {
66
74
  for (const param of tool.parameters) {
package/src/server.ts CHANGED
@@ -27,7 +27,7 @@ class MicrosoftGraphServer {
27
27
  });
28
28
 
29
29
  registerAuthTools(this.server, this.authManager);
30
- registerGraphTools(this.server, this.graphClient);
30
+ registerGraphTools(this.server, this.graphClient, this.options.readOnly);
31
31
  }
32
32
 
33
33
  async start(): Promise<void> {
@@ -36,6 +36,9 @@ class MicrosoftGraphServer {
36
36
  }
37
37
 
38
38
  logger.info('Microsoft 365 MCP Server starting...');
39
+ if (this.options.readOnly) {
40
+ logger.info('Server running in READ-ONLY mode. Write operations are disabled.');
41
+ }
39
42
 
40
43
  const transport = new StdioServerTransport();
41
44
  await this.server!.connect(transport);
@@ -0,0 +1,96 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { parseArgs } from '../src/cli.js';
3
+ import { registerGraphTools } from '../src/graph-tools.js';
4
+
5
+ vi.mock('../src/cli.js', () => {
6
+ const parseArgsMock = vi.fn();
7
+ return {
8
+ parseArgs: parseArgsMock,
9
+ };
10
+ });
11
+
12
+ vi.mock('../src/generated/client.js', () => {
13
+ return {
14
+ api: {
15
+ endpoints: [
16
+ {
17
+ alias: 'list-mail-messages',
18
+ method: 'get',
19
+ path: '/me/messages',
20
+ parameters: [],
21
+ },
22
+ {
23
+ alias: 'send-mail',
24
+ method: 'post',
25
+ path: '/me/sendMail',
26
+ parameters: [],
27
+ },
28
+ {
29
+ alias: 'delete-mail-message',
30
+ method: 'delete',
31
+ path: '/me/messages/{message-id}',
32
+ parameters: [],
33
+ },
34
+ ],
35
+ },
36
+ };
37
+ });
38
+
39
+ vi.mock('../src/logger.js', () => {
40
+ return {
41
+ default: {
42
+ info: vi.fn(),
43
+ error: vi.fn(),
44
+ },
45
+ };
46
+ });
47
+
48
+ describe('Read-Only Mode', () => {
49
+ let mockServer: any;
50
+
51
+ beforeEach(() => {
52
+ vi.clearAllMocks();
53
+
54
+ delete process.env.READ_ONLY;
55
+
56
+ mockServer = {
57
+ tool: vi.fn(),
58
+ };
59
+ });
60
+
61
+ afterEach(() => {
62
+ vi.resetAllMocks();
63
+ });
64
+
65
+ it('should respect --read-only flag from CLI', () => {
66
+ vi.mocked(parseArgs).mockReturnValue({ readOnly: true } as any);
67
+
68
+ const options = parseArgs();
69
+ expect(options.readOnly).toBe(true);
70
+
71
+ registerGraphTools(mockServer, {} as any, options.readOnly);
72
+
73
+ expect(mockServer.tool).toHaveBeenCalledTimes(1);
74
+
75
+ const toolCalls = mockServer.tool.mock.calls.map((call: any[]) => call[0]);
76
+ expect(toolCalls).toContain('list-mail-messages');
77
+ expect(toolCalls).not.toContain('send-mail');
78
+ expect(toolCalls).not.toContain('delete-mail-message');
79
+ });
80
+
81
+ it('should register all endpoints when not in read-only mode', () => {
82
+ vi.mocked(parseArgs).mockReturnValue({ readOnly: false } as any);
83
+
84
+ const options = parseArgs();
85
+ expect(options.readOnly).toBe(false);
86
+
87
+ registerGraphTools(mockServer, {} as any, options.readOnly);
88
+
89
+ expect(mockServer.tool).toHaveBeenCalledTimes(3);
90
+
91
+ const toolCalls = mockServer.tool.mock.calls.map((call: any[]) => call[0]);
92
+ expect(toolCalls).toContain('list-mail-messages');
93
+ expect(toolCalls).toContain('send-mail');
94
+ expect(toolCalls).toContain('delete-mail-message');
95
+ });
96
+ });