@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 +25 -0
- package/dist/auth.js +1 -1
- package/dist/cli.js +7 -2
- package/dist/graph-tools.js +5 -2
- package/dist/server.js +4 -1
- package/package.json +1 -1
- package/src/auth.ts +1 -1
- package/src/cli.ts +11 -2
- package/src/graph-tools.ts +10 -2
- package/src/server.ts +4 -1
- package/test/read-only.test.ts +96 -0
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/graph-tools.js
CHANGED
|
@@ -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
|
-
|
|
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
package/src/auth.ts
CHANGED
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
|
-
|
|
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
|
}
|
package/src/graph-tools.ts
CHANGED
|
@@ -58,9 +58,17 @@ interface CallToolResult {
|
|
|
58
58
|
[key: string]: unknown;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export function registerGraphTools(
|
|
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
|
-
|
|
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
|
+
});
|