UNPKG

bc-webclient-mcp

Version:

Model Context Protocol (MCP) server for Microsoft Dynamics 365 Business Central via WebUI protocol. Enables AI assistants to interact with BC through the web client protocol, supporting Card, List, and Document pages with full line item support and server

159 lines 5.5 kB
/** * Pure functions for BC handler parsing. * * No dependencies - easily testable in isolation. * Extracted from BCRawWebSocketClient per FUTURE_ERRATA.md Week 2.4 * * These utilities handle: * - Handler decompression (gzip → JSON) * - Compressed data extraction (multiple BC message formats) * - Session information extraction (recursive parameter search) */ import { gunzipSync } from 'zlib'; /** * Decompress gzip-compressed BC handler array. * * BC sends handler responses as base64-encoded gzip-compressed JSON arrays. * This function reverses that encoding. * * @param base64 Base64-encoded gzip data * @returns Decompressed handler array * @throws {Error} If decompression or JSON parsing fails * * @example * ```ts * const handlers = decompressHandlers('H4sIAAAAAAAA...'); * // [{ handlerType: 'DN.LogicalClientEventRaisingHandler', parameters: [...] }] * ``` */ export function decompressHandlers(base64) { const compressed = Buffer.from(base64, 'base64'); const decompressed = gunzipSync(compressed); const decompressedJson = decompressed.toString('utf-8'); const actualResponse = JSON.parse(decompressedJson); return Array.isArray(actualResponse) ? actualResponse : []; } export function extractCompressedData(message) { const msg = message; // Format 1: Async Message envelope with nested compressedResult if (msg.method === 'Message' && msg.params?.[0]?.compressedResult) { return msg.params[0].compressedResult; } // Format 2: Async Message envelope with compressedData (alternative field name) if (msg.method === 'Message' && msg.params?.[0]?.compressedData) { return msg.params[0].compressedData; } // Format 3: Top-level compressedResult if (msg.compressedResult) { return msg.compressedResult; } // Format 4: JSON-RPC result with compressedResult if (msg.result?.compressedResult) { return msg.result.compressedResult; } return null; } /** Session field names to extract from BC parameters */ const SESSION_FIELD_MAP = { ServerSessionId: 'serverSessionId', SessionKey: 'sessionKey', CompanyName: 'companyName', }; /** * Recursively search parameter tree for session fields. * * BC parameter structures can be deeply nested arrays and objects. * Session fields may be scattered across different nested structures. * This function collects ALL occurrences and merges them. */ function searchParamsForSessionFields(params, result) { if (Array.isArray(params)) { for (const item of params) { searchParamsForSessionFields(item, result); } return; } if (params && typeof params === 'object') { const obj = params; // Check for each session field for (const [bcField, resultField] of Object.entries(SESSION_FIELD_MAP)) { if (obj[bcField] && !result[resultField]) { // Type-safe assignment using indexed access const mutableResult = result; mutableResult[resultField] = obj[bcField]; } } // Recurse into object values for (const value of Object.values(obj)) { searchParamsForSessionFields(value, result); } } } /** * Find role center form ID from FormToShow handler. */ function findRoleCenterFormId(handlers) { const formToShowHandler = handlers.find(h => { if (h.handlerType !== 'DN.LogicalClientEventRaisingHandler') return false; if (h.parameters?.[0] !== 'FormToShow') return false; const formData = h.parameters?.[1]; return !!formData?.ServerId; }); const formData = formToShowHandler?.parameters?.[1]; return formData?.ServerId; } /** * Extract session information from handler array. * * BC embeds session info (ServerSessionId, SessionKey, CompanyName) deep * within handler parameters. This function recursively searches handler * parameter trees to find these values. * * Session info typically appears in OpenSession responses but may also * appear in other handlers during session establishment. * * @param handlers Array of BC handlers to search * @returns Session info object or null if not found * * @example * ```ts * const handlers = [ * { * handlerType: 'DN.SessionHandler', * parameters: [ * { ServerSessionId: 'abc123', SessionKey: 'key456', CompanyName: 'CRONUS' } * ] * } * ]; * * const info = extractSessionInfo(handlers); * // { serverSessionId: 'abc123', sessionKey: 'key456', companyName: 'CRONUS' } * ``` */ export function extractSessionInfo(handlers) { const sessionInfo = {}; // Collect session fields from all handler parameters for (const handler of handlers) { if (handler.parameters) { searchParamsForSessionFields(handler.parameters, sessionInfo); } } // Check if we found any session fields const hasSessionInfo = sessionInfo.serverSessionId || sessionInfo.sessionKey || sessionInfo.companyName; if (!hasSessionInfo) { return null; } // Add role center form ID if present sessionInfo.roleCenterFormId = findRoleCenterFormId(handlers); return sessionInfo; } export function extractOpenFormIds(message) { const msg = message; if (msg.method === 'Message' && msg.params?.[0]?.openFormIds) { return msg.params[0].openFormIds; } return null; } //# sourceMappingURL=handlers.js.map