ms365-mcp-server 1.0.0
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/LICENSE +21 -0
- package/README.md +497 -0
- package/bin/cli.js +247 -0
- package/dist/index.js +1251 -0
- package/dist/utils/api.js +43 -0
- package/dist/utils/credential-store.js +258 -0
- package/dist/utils/ms365-auth-enhanced.js +639 -0
- package/dist/utils/ms365-auth.js +363 -0
- package/dist/utils/ms365-operations.js +644 -0
- package/dist/utils/multi-user-auth.js +359 -0
- package/install.js +41 -0
- package/package.json +59 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
// Debug log file path
|
|
5
|
+
const LOG_FILE = path.join(os.tmpdir(), 'ms365-mcp-server.log');
|
|
6
|
+
/**
|
|
7
|
+
* Simple logging utility for MS365 MCP Server
|
|
8
|
+
*/
|
|
9
|
+
export class Logger {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.debugMode = false;
|
|
12
|
+
this.debugMode = process.argv.includes('--debug') || process.env.DEBUG === 'true';
|
|
13
|
+
}
|
|
14
|
+
log(message, ...args) {
|
|
15
|
+
const timestamp = new Date().toISOString();
|
|
16
|
+
const logMessage = `${timestamp} [INFO] ${message}`;
|
|
17
|
+
if (this.debugMode) {
|
|
18
|
+
console.error(logMessage, ...args);
|
|
19
|
+
}
|
|
20
|
+
this.writeToFile(logMessage, ...args);
|
|
21
|
+
}
|
|
22
|
+
error(message, error) {
|
|
23
|
+
const timestamp = new Date().toISOString();
|
|
24
|
+
const logMessage = `${timestamp} [ERROR] ${message}`;
|
|
25
|
+
if (this.debugMode) {
|
|
26
|
+
console.error(logMessage, error);
|
|
27
|
+
}
|
|
28
|
+
this.writeToFile(logMessage, error);
|
|
29
|
+
}
|
|
30
|
+
writeToFile(message, ...args) {
|
|
31
|
+
try {
|
|
32
|
+
let fullMessage = message;
|
|
33
|
+
if (args.length > 0) {
|
|
34
|
+
fullMessage += ' ' + args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
|
|
35
|
+
}
|
|
36
|
+
fs.appendFileSync(LOG_FILE, fullMessage + '\n');
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
// Silently fail if we can't write to log file
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export const logger = new Logger();
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import { logger } from './api.js';
|
|
5
|
+
// Service name for keychain storage
|
|
6
|
+
const SERVICE_NAME = 'ms365-mcp-server';
|
|
7
|
+
// Fallback directory for file-based storage
|
|
8
|
+
const FALLBACK_DIR = path.join(os.homedir(), '.ms365-mcp');
|
|
9
|
+
/**
|
|
10
|
+
* Secure credential store that uses OS keychain when available,
|
|
11
|
+
* with encrypted file fallback for cross-platform compatibility.
|
|
12
|
+
*/
|
|
13
|
+
export class CredentialStore {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.useKeychain = false;
|
|
16
|
+
this.keytar = null;
|
|
17
|
+
this.initialized = false;
|
|
18
|
+
this.initPromise = null;
|
|
19
|
+
this.ensureFallbackDir();
|
|
20
|
+
this.initPromise = this.initializeKeychain();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Ensure the credential store is initialized
|
|
24
|
+
*/
|
|
25
|
+
async ensureInitialized() {
|
|
26
|
+
if (!this.initialized && this.initPromise) {
|
|
27
|
+
await this.initPromise;
|
|
28
|
+
this.initialized = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Initialize keychain support if available
|
|
33
|
+
*/
|
|
34
|
+
async initializeKeychain() {
|
|
35
|
+
try {
|
|
36
|
+
// Try to load keytar for secure OS keychain storage
|
|
37
|
+
// Use eval to prevent TypeScript from checking the import at compile time
|
|
38
|
+
const keytarModule = await eval('import("keytar")');
|
|
39
|
+
this.keytar = keytarModule.default || keytarModule;
|
|
40
|
+
this.useKeychain = true;
|
|
41
|
+
logger.log('OS keychain support enabled');
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
// Keytar might not be available on all systems
|
|
45
|
+
logger.log('OS keychain not available, using file fallback');
|
|
46
|
+
this.useKeychain = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Ensure fallback directory exists
|
|
51
|
+
*/
|
|
52
|
+
ensureFallbackDir() {
|
|
53
|
+
if (!fs.existsSync(FALLBACK_DIR)) {
|
|
54
|
+
fs.mkdirSync(FALLBACK_DIR, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Store credentials securely
|
|
59
|
+
*/
|
|
60
|
+
async setCredentials(account, credentials) {
|
|
61
|
+
await this.ensureInitialized();
|
|
62
|
+
const credentialString = JSON.stringify(credentials);
|
|
63
|
+
if (this.useKeychain && this.keytar) {
|
|
64
|
+
try {
|
|
65
|
+
await this.keytar.setPassword(SERVICE_NAME, account, credentialString);
|
|
66
|
+
logger.log(`Stored credentials for ${account} in OS keychain`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
logger.error(`Failed to store in keychain for ${account}:`, error);
|
|
71
|
+
// Fall through to file storage
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Fallback to file storage
|
|
75
|
+
await this.setCredentialsFile(account, credentials);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Retrieve credentials securely
|
|
79
|
+
*/
|
|
80
|
+
async getCredentials(account) {
|
|
81
|
+
await this.ensureInitialized();
|
|
82
|
+
if (this.useKeychain && this.keytar) {
|
|
83
|
+
try {
|
|
84
|
+
const credentialString = await this.keytar.getPassword(SERVICE_NAME, account);
|
|
85
|
+
if (credentialString) {
|
|
86
|
+
logger.log(`Retrieved credentials for ${account} from OS keychain`);
|
|
87
|
+
return JSON.parse(credentialString);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
logger.error(`Failed to retrieve from keychain for ${account}:`, error);
|
|
92
|
+
// Fall through to file storage
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Fallback to file storage
|
|
96
|
+
return await this.getCredentialsFile(account);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Delete credentials securely
|
|
100
|
+
*/
|
|
101
|
+
async deleteCredentials(account) {
|
|
102
|
+
await this.ensureInitialized();
|
|
103
|
+
let deleted = false;
|
|
104
|
+
if (this.useKeychain && this.keytar) {
|
|
105
|
+
try {
|
|
106
|
+
deleted = await this.keytar.deletePassword(SERVICE_NAME, account);
|
|
107
|
+
if (deleted) {
|
|
108
|
+
logger.log(`Deleted credentials for ${account} from OS keychain`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
logger.error(`Failed to delete from keychain for ${account}:`, error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Also try file storage
|
|
116
|
+
const fileDeleted = await this.deleteCredentialsFile(account);
|
|
117
|
+
return deleted || fileDeleted;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* List all stored accounts
|
|
121
|
+
*/
|
|
122
|
+
async listAccounts() {
|
|
123
|
+
await this.ensureInitialized();
|
|
124
|
+
const accounts = new Set();
|
|
125
|
+
// Get accounts from keychain (if available)
|
|
126
|
+
if (this.useKeychain && this.keytar) {
|
|
127
|
+
try {
|
|
128
|
+
const keychainAccounts = await this.keytar.findCredentials(SERVICE_NAME);
|
|
129
|
+
keychainAccounts.forEach((cred) => accounts.add(cred.account));
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
logger.error('Failed to list keychain accounts:', error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Get accounts from file storage
|
|
136
|
+
const fileAccounts = await this.listFileAccounts();
|
|
137
|
+
fileAccounts.forEach(account => accounts.add(account));
|
|
138
|
+
return Array.from(accounts);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* File-based credential storage (fallback)
|
|
142
|
+
*/
|
|
143
|
+
async setCredentialsFile(account, credentials) {
|
|
144
|
+
try {
|
|
145
|
+
const filePath = path.join(FALLBACK_DIR, `token_${this.sanitizeFilename(account)}.json`);
|
|
146
|
+
const encryptedData = this.simpleEncrypt(JSON.stringify(credentials));
|
|
147
|
+
fs.writeFileSync(filePath, encryptedData);
|
|
148
|
+
logger.log(`Stored credentials for ${account} in file`);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
logger.error(`Failed to store credentials in file for ${account}:`, error);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* File-based credential retrieval (fallback)
|
|
157
|
+
*/
|
|
158
|
+
async getCredentialsFile(account) {
|
|
159
|
+
try {
|
|
160
|
+
const filePath = path.join(FALLBACK_DIR, `token_${this.sanitizeFilename(account)}.json`);
|
|
161
|
+
if (!fs.existsSync(filePath)) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const encryptedData = fs.readFileSync(filePath, 'utf8');
|
|
165
|
+
const decryptedData = this.simpleDecrypt(encryptedData);
|
|
166
|
+
logger.log(`Retrieved credentials for ${account} from file`);
|
|
167
|
+
return JSON.parse(decryptedData);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
logger.error(`Failed to retrieve credentials from file for ${account}:`, error);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* File-based credential deletion (fallback)
|
|
176
|
+
*/
|
|
177
|
+
async deleteCredentialsFile(account) {
|
|
178
|
+
try {
|
|
179
|
+
const filePath = path.join(FALLBACK_DIR, `token_${this.sanitizeFilename(account)}.json`);
|
|
180
|
+
if (fs.existsSync(filePath)) {
|
|
181
|
+
fs.unlinkSync(filePath);
|
|
182
|
+
logger.log(`Deleted credentials for ${account} from file`);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
logger.error(`Failed to delete credentials from file for ${account}:`, error);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* List accounts from file storage
|
|
194
|
+
*/
|
|
195
|
+
async listFileAccounts() {
|
|
196
|
+
try {
|
|
197
|
+
const files = fs.readdirSync(FALLBACK_DIR);
|
|
198
|
+
return files
|
|
199
|
+
.filter(file => file.startsWith('token_') && file.endsWith('.json'))
|
|
200
|
+
.map(file => file.replace('token_', '').replace('.json', ''))
|
|
201
|
+
.map(filename => this.unsanitizeFilename(filename));
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
logger.error('Failed to list file accounts:', error);
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Sanitize account name for file storage
|
|
210
|
+
*/
|
|
211
|
+
sanitizeFilename(account) {
|
|
212
|
+
return account.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Reverse filename sanitization
|
|
216
|
+
*/
|
|
217
|
+
unsanitizeFilename(filename) {
|
|
218
|
+
// This is a simplified reverse - in practice, we'd need a more robust mapping
|
|
219
|
+
return filename.replace(/_/g, '@');
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Simple encryption for file storage (base64 + basic obfuscation)
|
|
223
|
+
* Note: This is not cryptographically secure, just obfuscation
|
|
224
|
+
*/
|
|
225
|
+
simpleEncrypt(data) {
|
|
226
|
+
const key = 'ms365-mcp-server-key';
|
|
227
|
+
let encrypted = '';
|
|
228
|
+
for (let i = 0; i < data.length; i++) {
|
|
229
|
+
encrypted += String.fromCharCode(data.charCodeAt(i) ^ key.charCodeAt(i % key.length));
|
|
230
|
+
}
|
|
231
|
+
return Buffer.from(encrypted).toString('base64');
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Simple decryption for file storage
|
|
235
|
+
*/
|
|
236
|
+
simpleDecrypt(encryptedData) {
|
|
237
|
+
const key = 'ms365-mcp-server-key';
|
|
238
|
+
const encrypted = Buffer.from(encryptedData, 'base64').toString();
|
|
239
|
+
let decrypted = '';
|
|
240
|
+
for (let i = 0; i < encrypted.length; i++) {
|
|
241
|
+
decrypted += String.fromCharCode(encrypted.charCodeAt(i) ^ key.charCodeAt(i % key.length));
|
|
242
|
+
}
|
|
243
|
+
return decrypted;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Check if keychain is available
|
|
247
|
+
*/
|
|
248
|
+
isKeychainAvailable() {
|
|
249
|
+
return this.useKeychain && this.keytar !== null;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get storage method being used
|
|
253
|
+
*/
|
|
254
|
+
getStorageMethod() {
|
|
255
|
+
return this.useKeychain ? 'OS Keychain' : 'Encrypted File';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export const credentialStore = new CredentialStore();
|