@softeria/ms-365-mcp-server 0.4.2 → 0.4.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Microsoft 365 MCP Server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,7 +14,8 @@
14
14
  "dev": "tsx src/index.ts",
15
15
  "format": "prettier --write \"**/*.{ts,mts,js,mjs,json,md}\"",
16
16
  "release": "ts-node --esm bin/release.mts",
17
- "inspect": "npx @modelcontextprotocol/inspector tsx src/index.ts"
17
+ "inspect": "npx @modelcontextprotocol/inspector tsx src/index.ts",
18
+ "prepublishOnly": "npm run build"
18
19
  },
19
20
  "keywords": [
20
21
  "microsoft",
@@ -104,7 +104,7 @@
104
104
  ]
105
105
  },
106
106
  {
107
- "pathPattern": "/drives",
107
+ "pathPattern": "/me/drives",
108
108
  "method": "get",
109
109
  "toolName": "list-drives",
110
110
  "scopes": [
@@ -2,11 +2,16 @@
2
2
 
3
3
  This directory contains the generated TypeScript client for the Microsoft 365 API based on the OpenAPI specification.
4
4
 
5
+ > **Important Note for NPM Package Users**:
6
+ > The source file `client.ts` (approximately 1MB) is excluded from the npm package to reduce package size,
7
+ > but the compiled JavaScript file `client.js` is included. This means the package is fully functional,
8
+ > but you won't see the TypeScript source in the node_modules directory.
9
+
5
10
  ## The Evolution
6
11
 
7
12
  ### Initial Challenge
8
13
 
9
- Our initial approach used the full MS 365 OpenAPI specification file directly. This created two significant problems:
14
+ Our initial approach used the full MS 365 OpenAPI specification file directly. This created several significant problems:
10
15
 
11
16
  - The spec file was a whopping 45MB in size
12
17
  - It had to be included in the npm package
@@ -1,34 +0,0 @@
1
- name: Build
2
-
3
- on:
4
- push:
5
- branches: [ main ]
6
- pull_request:
7
- branches: [ main ]
8
- workflow_dispatch:
9
-
10
- jobs:
11
- build:
12
- runs-on: ubuntu-latest
13
-
14
- strategy:
15
- matrix:
16
- node-version: [ 18.x, 20.x ]
17
-
18
- steps:
19
- - uses: actions/checkout@v4
20
-
21
- - name: Use Node.js ${{ matrix.node-version }}
22
- uses: actions/setup-node@v4
23
- with:
24
- node-version: ${{ matrix.node-version }}
25
- cache: 'npm'
26
-
27
- - name: Install dependencies
28
- run: npm ci
29
-
30
- - name: Build TypeScript
31
- run: npm run build
32
-
33
- - name: Run tests
34
- run: npm test
@@ -1,32 +0,0 @@
1
- name: Node.js Package
2
-
3
- on:
4
- release:
5
- types: [ created ]
6
-
7
- jobs:
8
- build:
9
- runs-on: ubuntu-latest
10
- steps:
11
- - uses: actions/checkout@v4
12
- - uses: actions/setup-node@v4
13
- with:
14
- node-version: 20
15
- - run: npm ci
16
- - run: npm run build
17
- - run: npm test
18
-
19
- publish-npm:
20
- needs: build
21
- runs-on: ubuntu-latest
22
- steps:
23
- - uses: actions/checkout@v4
24
- - uses: actions/setup-node@v4
25
- with:
26
- node-version: 20
27
- registry-url: https://registry.npmjs.org/
28
- - run: npm ci
29
- - run: npm run build
30
- - run: npm publish
31
- env:
32
- NODE_AUTH_TOKEN: ${{secrets.npm_token}}
package/.prettierrc DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "semi": true,
3
- "singleQuote": true,
4
- "trailingComma": "es5",
5
- "printWidth": 100,
6
- "tabWidth": 2
7
- }
package/src/auth-tools.ts DELETED
@@ -1,89 +0,0 @@
1
- import { z } from 'zod';
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import AuthManager from './auth.js';
4
-
5
- export function registerAuthTools(server: McpServer, authManager: AuthManager): void {
6
- server.tool(
7
- 'login',
8
- {
9
- force: z.boolean().default(false).describe('Force a new login even if already logged in'),
10
- },
11
- async ({ force }) => {
12
- try {
13
- if (!force) {
14
- const loginStatus = await authManager.testLogin();
15
- if (loginStatus.success) {
16
- return {
17
- content: [
18
- {
19
- type: 'text',
20
- text: JSON.stringify({
21
- status: 'Already logged in',
22
- ...loginStatus,
23
- }),
24
- },
25
- ],
26
- };
27
- }
28
- }
29
-
30
- const text = await new Promise<string>((r) => {
31
- authManager.acquireTokenByDeviceCode(r);
32
- });
33
- return {
34
- content: [
35
- {
36
- type: 'text',
37
- text,
38
- },
39
- ],
40
- };
41
- } catch (error) {
42
- return {
43
- content: [
44
- {
45
- type: 'text',
46
- text: JSON.stringify({ error: `Authentication failed: ${(error as Error).message}` }),
47
- },
48
- ],
49
- };
50
- }
51
- }
52
- );
53
-
54
- server.tool('logout', {}, async () => {
55
- try {
56
- await authManager.logout();
57
- return {
58
- content: [
59
- {
60
- type: 'text',
61
- text: JSON.stringify({ message: 'Logged out successfully' }),
62
- },
63
- ],
64
- };
65
- } catch (error) {
66
- return {
67
- content: [
68
- {
69
- type: 'text',
70
- text: JSON.stringify({ error: 'Logout failed' }),
71
- },
72
- ],
73
- };
74
- }
75
- });
76
-
77
- server.tool('verify-login', async () => {
78
- const testResult = await authManager.testLogin();
79
-
80
- return {
81
- content: [
82
- {
83
- type: 'text',
84
- text: JSON.stringify(testResult),
85
- },
86
- ],
87
- };
88
- });
89
- }
package/src/auth.ts DELETED
@@ -1,267 +0,0 @@
1
- import { PublicClientApplication } from '@azure/msal-node';
2
- import type { Configuration } from '@azure/msal-node';
3
- import keytar from 'keytar';
4
- import { fileURLToPath } from 'url';
5
- import path from 'path';
6
- import fs from 'fs';
7
- import logger from './logger.js';
8
-
9
- const endpoints = await import('./endpoints.json', {
10
- with: { type: 'json' },
11
- });
12
-
13
- const SERVICE_NAME = 'ms-365-mcp-server';
14
- const TOKEN_CACHE_ACCOUNT = 'msal-token-cache';
15
- const FALLBACK_DIR = path.dirname(fileURLToPath(import.meta.url));
16
- const FALLBACK_PATH = path.join(FALLBACK_DIR, '..', '.token-cache.json');
17
-
18
- const DEFAULT_CONFIG: Configuration = {
19
- auth: {
20
- clientId: '084a3e9f-a9f4-43f7-89f9-d229cf97853e',
21
- authority: 'https://login.microsoftonline.com/common',
22
- },
23
- };
24
-
25
- interface ScopeHierarchy {
26
- [key: string]: string[];
27
- }
28
-
29
- const SCOPE_HIERARCHY: ScopeHierarchy = {
30
- 'Mail.ReadWrite': ['Mail.Read', 'Mail.Send'],
31
- 'Calendars.ReadWrite': ['Calendars.Read'],
32
- 'Files.ReadWrite': ['Files.Read'],
33
- 'Tasks.ReadWrite': ['Tasks.Read'],
34
- 'Contacts.ReadWrite': ['Contacts.Read'],
35
- };
36
-
37
- function buildScopesFromEndpoints(): string[] {
38
- const scopesSet = new Set<string>();
39
-
40
- endpoints.default.forEach((endpoint) => {
41
- if (endpoint.scopes && Array.isArray(endpoint.scopes)) {
42
- endpoint.scopes.forEach((scope) => scopesSet.add(scope));
43
- }
44
- });
45
-
46
- Object.entries(SCOPE_HIERARCHY).forEach(([higherScope, lowerScopes]) => {
47
- if (lowerScopes.every((scope) => scopesSet.has(scope))) {
48
- lowerScopes.forEach((scope) => scopesSet.delete(scope));
49
- scopesSet.add(higherScope);
50
- }
51
- });
52
-
53
- return Array.from(scopesSet);
54
- }
55
-
56
- interface LoginTestResult {
57
- success: boolean;
58
- message: string;
59
- userData?: {
60
- displayName: string;
61
- userPrincipalName: string;
62
- };
63
- }
64
-
65
- class AuthManager {
66
- private config: Configuration;
67
- private scopes: string[];
68
- private msalApp: PublicClientApplication;
69
- private accessToken: string | null;
70
- private tokenExpiry: number | null;
71
-
72
- constructor(
73
- config: Configuration = DEFAULT_CONFIG,
74
- scopes: string[] = buildScopesFromEndpoints()
75
- ) {
76
- logger.info(`And scopes are ${scopes.join(', ')}`, scopes);
77
- this.config = config;
78
- this.scopes = scopes;
79
- this.msalApp = new PublicClientApplication(this.config);
80
- this.accessToken = null;
81
- this.tokenExpiry = null;
82
- }
83
-
84
- async loadTokenCache(): Promise<void> {
85
- try {
86
- let cacheData: string | undefined;
87
-
88
- try {
89
- const cachedData = await keytar.getPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
90
- if (cachedData) {
91
- cacheData = cachedData;
92
- }
93
- } catch (keytarError) {
94
- logger.warn(
95
- `Keychain access failed, falling back to file storage: ${(keytarError as Error).message}`
96
- );
97
- }
98
-
99
- if (!cacheData && fs.existsSync(FALLBACK_PATH)) {
100
- cacheData = fs.readFileSync(FALLBACK_PATH, 'utf8');
101
- }
102
-
103
- if (cacheData) {
104
- this.msalApp.getTokenCache().deserialize(cacheData);
105
- }
106
- } catch (error) {
107
- logger.error(`Error loading token cache: ${(error as Error).message}`);
108
- }
109
- }
110
-
111
- async saveTokenCache(): Promise<void> {
112
- try {
113
- const cacheData = this.msalApp.getTokenCache().serialize();
114
-
115
- try {
116
- await keytar.setPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT, cacheData);
117
- } catch (keytarError) {
118
- logger.warn(
119
- `Keychain save failed, falling back to file storage: ${(keytarError as Error).message}`
120
- );
121
-
122
- fs.writeFileSync(FALLBACK_PATH, cacheData);
123
- }
124
- } catch (error) {
125
- logger.error(`Error saving token cache: ${(error as Error).message}`);
126
- }
127
- }
128
-
129
- async getToken(forceRefresh = false): Promise<string | null> {
130
- if (this.accessToken && this.tokenExpiry && this.tokenExpiry > Date.now() && !forceRefresh) {
131
- return this.accessToken;
132
- }
133
-
134
- const accounts = await this.msalApp.getTokenCache().getAllAccounts();
135
-
136
- if (accounts.length > 0) {
137
- const silentRequest = {
138
- account: accounts[0],
139
- scopes: this.scopes,
140
- };
141
-
142
- try {
143
- const response = await this.msalApp.acquireTokenSilent(silentRequest);
144
- this.accessToken = response.accessToken;
145
- this.tokenExpiry = response.expiresOn ? new Date(response.expiresOn).getTime() : null;
146
- return this.accessToken;
147
- } catch (error) {
148
- logger.info('Silent token acquisition failed, using device code flow');
149
- }
150
- }
151
-
152
- throw new Error('No valid token found');
153
- }
154
-
155
- async acquireTokenByDeviceCode(hack?: (message: string) => void): Promise<string | null> {
156
- const deviceCodeRequest = {
157
- scopes: this.scopes,
158
- deviceCodeCallback: (response: { message: string }) => {
159
- const text = ['\n', response.message, '\n'].join('');
160
- if (hack) {
161
- hack(text + 'After login run the "verify login" command');
162
- } else {
163
- console.log(text);
164
- }
165
- logger.info('Device code login initiated');
166
- },
167
- };
168
-
169
- try {
170
- logger.info('Requesting device code...');
171
- const response = await this.msalApp.acquireTokenByDeviceCode(deviceCodeRequest);
172
- logger.info('Device code login successful');
173
- this.accessToken = response?.accessToken || null;
174
- this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
175
- await this.saveTokenCache();
176
- return this.accessToken;
177
- } catch (error) {
178
- logger.error(`Error in device code flow: ${(error as Error).message}`);
179
- throw error;
180
- }
181
- }
182
-
183
- async testLogin(): Promise<LoginTestResult> {
184
- try {
185
- logger.info('Testing login...');
186
- const token = await this.getToken();
187
-
188
- if (!token) {
189
- logger.error('Login test failed - no token received');
190
- return {
191
- success: false,
192
- message: 'Login failed - no token received',
193
- };
194
- }
195
-
196
- logger.info('Token retrieved successfully, testing Graph API access...');
197
-
198
- try {
199
- const response = await fetch('https://graph.microsoft.com/v1.0/me', {
200
- headers: {
201
- Authorization: `Bearer ${token}`,
202
- },
203
- });
204
-
205
- if (response.ok) {
206
- const userData = await response.json();
207
- logger.info('Graph API user data fetch successful');
208
- return {
209
- success: true,
210
- message: 'Login successful',
211
- userData: {
212
- displayName: userData.displayName,
213
- userPrincipalName: userData.userPrincipalName,
214
- },
215
- };
216
- } else {
217
- const errorText = await response.text();
218
- logger.error(`Graph API user data fetch failed: ${response.status} - ${errorText}`);
219
- return {
220
- success: false,
221
- message: `Login successful but Graph API access failed: ${response.status}`,
222
- };
223
- }
224
- } catch (graphError) {
225
- logger.error(`Error fetching user data: ${(graphError as Error).message}`);
226
- return {
227
- success: false,
228
- message: `Login successful but Graph API access failed: ${(graphError as Error).message}`,
229
- };
230
- }
231
- } catch (error) {
232
- logger.error(`Login test failed: ${(error as Error).message}`);
233
- return {
234
- success: false,
235
- message: `Login failed: ${(error as Error).message}`,
236
- };
237
- }
238
- }
239
-
240
- async logout(): Promise<boolean> {
241
- try {
242
- const accounts = await this.msalApp.getTokenCache().getAllAccounts();
243
- for (const account of accounts) {
244
- await this.msalApp.getTokenCache().removeAccount(account);
245
- }
246
- this.accessToken = null;
247
- this.tokenExpiry = null;
248
-
249
- try {
250
- await keytar.deletePassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
251
- } catch (keytarError) {
252
- logger.warn(`Keychain deletion failed: ${(keytarError as Error).message}`);
253
- }
254
-
255
- if (fs.existsSync(FALLBACK_PATH)) {
256
- fs.unlinkSync(FALLBACK_PATH);
257
- }
258
-
259
- return true;
260
- } catch (error) {
261
- logger.error(`Error during logout: ${(error as Error).message}`);
262
- throw error;
263
- }
264
- }
265
- }
266
-
267
- export default AuthManager;
package/src/cli.ts DELETED
@@ -1,42 +0,0 @@
1
- import { Command } from 'commander';
2
- import { readFileSync } from 'fs';
3
- import path from 'path';
4
- import { fileURLToPath } from 'url';
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- const packageJsonPath = path.join(__dirname, '..', 'package.json');
8
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
9
- const version = packageJson.version;
10
-
11
- const program = new Command();
12
-
13
- program
14
- .name('ms-365-mcp-server')
15
- .description('Microsoft 365 MCP Server')
16
- .version(version)
17
- .option('-v', 'Enable verbose logging')
18
- .option('--login', 'Login using device code flow')
19
- .option('--logout', 'Log out and clear saved credentials')
20
- .option('--verify-login', 'Verify login without starting the server')
21
- .option('--read-only', 'Start server in read-only mode, disabling write operations');
22
-
23
- export interface CommandOptions {
24
- v?: boolean;
25
- login?: boolean;
26
- logout?: boolean;
27
- verifyLogin?: boolean;
28
- readOnly?: boolean;
29
-
30
- [key: string]: any;
31
- }
32
-
33
- export function parseArgs(): CommandOptions {
34
- program.parse();
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;
42
- }