UNPKG

@softeria/ms-365-mcp-server

Version:

Microsoft 365 MCP Server

294 lines (253 loc) 7.93 kB
import logger from './logger.mjs'; class GraphClient { constructor(authManager) { this.authManager = authManager; this.sessions = new Map(); } async createSession(filePath) { try { if (!filePath) { logger.error('No file path provided for Excel session'); return null; } if (this.sessions.has(filePath)) { return this.sessions.get(filePath); } logger.info(`Creating new Excel session for file: ${filePath}`); const accessToken = await this.authManager.getToken(); const response = await fetch( `https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/createSession`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ persistChanges: true }), } ); if (!response.ok) { const errorText = await response.text(); logger.error(`Failed to create session: ${response.status} - ${errorText}`); return null; } const result = await response.json(); logger.info(`Session created successfully for file: ${filePath}`); this.sessions.set(filePath, result.id); return result.id; } catch (error) { logger.error(`Error creating Excel session: ${error}`); return null; } } async graphRequest(endpoint, options = {}) { try { let accessToken = await this.authManager.getToken(); let url; let sessionId = null; if ( options.excelFile && !endpoint.startsWith('/drive') && !endpoint.startsWith('/users') && !endpoint.startsWith('/me') ) { sessionId = this.sessions.get(options.excelFile); if (!sessionId) { sessionId = await this.createSession(options.excelFile); } url = `https://graph.microsoft.com/v1.0/me/drive/root:${options.excelFile}:${endpoint}`; } else if ( endpoint.startsWith('/drive') || endpoint.startsWith('/users') || endpoint.startsWith('/me') ) { url = `https://graph.microsoft.com/v1.0${endpoint}`; } else { logger.error('Excel operation requested without specifying a file'); return { content: [ { type: 'text', text: JSON.stringify({ error: 'No Excel file specified for this operation' }), }, ], }; } const headers = { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', ...(sessionId && { 'workbook-session-id': sessionId }), ...options.headers, }; const response = await fetch(url, { headers, ...options, }); if (response.status === 401) { logger.info('Access token expired, refreshing...'); const newToken = await this.authManager.getToken(true); if ( options.excelFile && !endpoint.startsWith('/drive') && !endpoint.startsWith('/users') && !endpoint.startsWith('/me') ) { sessionId = await this.createSession(options.excelFile); } headers.Authorization = `Bearer ${newToken}`; if ( sessionId && !endpoint.startsWith('/drive') && !endpoint.startsWith('/users') && !endpoint.startsWith('/me') ) { headers['workbook-session-id'] = sessionId; } const retryResponse = await fetch(url, { headers, ...options, }); if (!retryResponse.ok) { throw new Error(`Graph API error: ${retryResponse.status} ${await retryResponse.text()}`); } return this.formatResponse(retryResponse, options.rawResponse); } if (!response.ok) { throw new Error(`Graph API error: ${response.status} ${await response.text()}`); } return this.formatResponse(response, options.rawResponse); } catch (error) { logger.error(`Error in Graph API request: ${error}`); return { content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }], }; } } async formatResponse(response, rawResponse = false) { try { if (response.status === 204) { return { content: [ { type: 'text', text: JSON.stringify({ message: 'Operation completed successfully', }), }, ], }; } if (rawResponse) { const contentType = response.headers.get('content-type'); if (contentType && contentType.startsWith('text/')) { const text = await response.text(); return { content: [{ type: 'text', text }], }; } return { content: [ { type: 'text', text: JSON.stringify({ message: 'Binary file content received', contentType: contentType, contentLength: response.headers.get('content-length'), }), }, ], }; } const result = await response.json(); const removeODataProps = (obj) => { if (!obj || typeof obj !== 'object') return; if (Array.isArray(obj)) { obj.forEach((item) => removeODataProps(item)); } else { Object.keys(obj).forEach((key) => { if (key.startsWith('@odata')) { delete obj[key]; } else if (typeof obj[key] === 'object') { removeODataProps(obj[key]); } }); } }; removeODataProps(result); return { content: [{ type: 'text', text: JSON.stringify(result) }], }; } catch (error) { logger.error(`Error formatting response: ${error}`); return { content: [{ type: 'text', text: JSON.stringify({ message: 'Success' }) }], }; } } async closeSession(filePath) { if (!filePath || !this.sessions.has(filePath)) { return { content: [ { type: 'text', text: JSON.stringify({ message: 'No active session for the specified file' }), }, ], }; } const sessionId = this.sessions.get(filePath); try { const accessToken = await this.authManager.getToken(); const response = await fetch( `https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/closeSession`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', 'workbook-session-id': sessionId, }, } ); if (response.ok) { this.sessions.delete(filePath); return { content: [ { type: 'text', text: JSON.stringify({ message: `Session for ${filePath} closed successfully` }), }, ], }; } else { throw new Error(`Failed to close session: ${response.status}`); } } catch (error) { logger.error(`Error closing session: ${error}`); return { content: [ { type: 'text', text: JSON.stringify({ error: `Failed to close session for ${filePath}` }), }, ], }; } } async closeAllSessions() { const results = []; for (const [filePath, _] of this.sessions) { const result = await this.closeSession(filePath); results.push(result); } return { content: [ { type: 'text', text: JSON.stringify({ message: 'All sessions closed', results }), }, ], }; } } export default GraphClient;