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

229 lines 8.01 kB
/** * Workflow State Manager * * Tracks business workflow execution state across BC sessions. * This provides a higher-level abstraction on top of SessionStateManager * to track multi-step business processes like "create_sales_invoice" or "post_sales_order". * * Architecture: * - WorkflowStateManager: Manages workflow instances (this file) * - SessionStateManager: Manages BC sessions and pages * - WorkflowContext: Tracks current state, history, errors for a workflow * * NOTE: This is ephemeral (in-memory) and will reset when the process restarts. */ import { v4 as uuidv4 } from 'uuid'; /** * WorkflowStateManager tracks business workflow execution state. * * This is a singleton to maintain consistent state across the application. * Provides higher-level workflow tracking on top of SessionStateManager. */ export class WorkflowStateManager { logger; static instance; /** * Gets the singleton instance. * @param logger - Optional logger for debug logging */ static getInstance(logger) { if (!WorkflowStateManager.instance) { WorkflowStateManager.instance = new WorkflowStateManager(logger); } return WorkflowStateManager.instance; } /** * Resets the singleton instance (primarily for testing). */ static resetInstance() { WorkflowStateManager.instance = undefined; } workflows = new Map(); constructor(logger) { this.logger = logger; } /** * Creates a new workflow. * @param input - Workflow creation parameters * @returns The newly created workflow context */ createWorkflow(input) { const workflowId = uuidv4(); const now = new Date().toISOString(); const workflow = { workflowId, sessionId: input.sessionId, goal: input.goal, parameters: input.parameters || {}, status: 'active', createdAt: now, updatedAt: now, operations: [], errors: [], }; this.workflows.set(workflowId, workflow); this.logger?.debug('Created new workflow', { workflowId, goal: input.goal, sessionId: input.sessionId }); return workflow; } /** * Gets a workflow by ID. * @param workflowId - The workflow ID * @returns The workflow context or undefined */ getWorkflow(workflowId) { return this.workflows.get(workflowId); } /** * Gets all workflows for a session. * @param sessionId - The session ID * @returns Array of workflows in the session */ getWorkflowsBySession(sessionId) { return Array.from(this.workflows.values()).filter((w) => w.sessionId === sessionId); } /** * Gets all active workflows (status = 'active'). * @returns Array of active workflows */ getActiveWorkflows() { return Array.from(this.workflows.values()).filter((w) => w.status === 'active'); } /** * Updates workflow state. * Creates a new immutable workflow context with updated fields. * @param workflowId - The workflow ID * @param update - State updates to apply * @returns The updated workflow context or undefined if not found */ updateWorkflowState(workflowId, update) { const existing = this.workflows.get(workflowId); if (!existing) { this.logger?.warn('Workflow not found for update', { workflowId }); return undefined; } // Build updated errors array let errors = existing.errors; if (update.clearErrors) { errors = []; } else if (update.appendError) { errors = [...existing.errors, update.appendError]; } // Create updated workflow (immutable pattern) const updated = { ...existing, status: update.status ?? existing.status, currentPageContextId: update.currentPageContextId ?? existing.currentPageContextId, currentPageId: update.currentPageId ?? existing.currentPageId, focusedRecordKeys: update.focusedRecordKeys ?? existing.focusedRecordKeys, unsavedChanges: update.unsavedChanges ?? existing.unsavedChanges, errors, lastError: errors.length > 0 ? errors[errors.length - 1] : existing.lastError, updatedAt: new Date().toISOString(), }; this.workflows.set(workflowId, updated); this.logger?.debug('Updated workflow state', { workflowId, update }); return updated; } /** * Records a completed operation in the workflow history. * @param workflowId - The workflow ID * @param operation - Operation details (without operationId and timestamp - will be auto-generated) * @returns The updated workflow context or undefined if not found */ recordOperation(workflowId, operation) { const existing = this.workflows.get(workflowId); if (!existing) { this.logger?.warn('Workflow not found for operation recording', { workflowId }); return undefined; } // Create operation with ID and timestamp const completedOperation = { operationId: uuidv4(), timestamp: new Date().toISOString(), ...operation, }; // Create updated workflow with new operation const updated = { ...existing, operations: [...existing.operations, completedOperation], lastOperation: completedOperation, updatedAt: new Date().toISOString(), }; this.workflows.set(workflowId, updated); this.logger?.debug('Recorded workflow operation', { workflowId, tool: operation.tool, success: operation.result.success, }); return updated; } /** * Completes a workflow successfully. * @param workflowId - The workflow ID * @returns The updated workflow context or undefined if not found */ completeWorkflow(workflowId) { return this.updateWorkflowState(workflowId, { status: 'completed' }); } /** * Fails a workflow with an error message. * @param workflowId - The workflow ID * @param error - Error message * @returns The updated workflow context or undefined if not found */ failWorkflow(workflowId, error) { const updated = this.updateWorkflowState(workflowId, { status: 'failed', appendError: error, }); this.logger?.warn('Workflow failed', { workflowId, error }); return updated; } /** * Cancels a workflow. * @param workflowId - The workflow ID * @returns The updated workflow context or undefined if not found */ cancelWorkflow(workflowId) { return this.updateWorkflowState(workflowId, { status: 'cancelled' }); } /** * Deletes a workflow. * @param workflowId - The workflow ID * @returns True if deleted, false if not found */ deleteWorkflow(workflowId) { const deleted = this.workflows.delete(workflowId); if (deleted) { this.logger?.debug('Deleted workflow', { workflowId }); } else { this.logger?.debug('Workflow not found for deletion', { workflowId }); } return deleted; } /** * Gets a snapshot of all workflows. * @returns Immutable snapshot of current workflow state */ getSnapshot() { return { workflows: Array.from(this.workflows.values()), }; } /** * Gets the number of workflows. */ getWorkflowCount() { return this.workflows.size; } /** * Clears all workflows (primarily for testing). */ clear() { this.workflows.clear(); this.logger?.debug('Cleared all workflows'); } } //# sourceMappingURL=workflow-state-manager.js.map