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.
@@ -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();