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
261 lines • 10.1 kB
TypeScript
/**
* Page Data Extractor
*
* Extracts actual data records from BC pages (both card and list types).
* Uses patterns from Tell Me search for list data extraction.
*/
import type { Result } from '../core/result.js';
import type { BCError } from '../core/errors.js';
import type { LogicalForm } from '../types/bc-types.js';
/**
* A single field value in a record.
*/
export interface FieldValue {
value: string | number | boolean | null;
displayValue?: string;
type: 'string' | 'number' | 'boolean' | 'date';
}
/**
* A single record (row) from a page.
*/
export interface PageRecord {
bookmark?: string;
fields: Record<string, FieldValue>;
}
/**
* Result of page data extraction.
*/
export interface PageDataExtractionResult {
pageType: 'card' | 'list' | 'document';
records: PageRecord[];
totalCount: number;
}
/**
* Document page lines block (e.g., Sales Lines, Purchase Lines).
* Each block represents a repeater control containing line items.
*/
export interface DocumentLinesBlock {
/** Repeater control path (e.g., "server:c[2]/c[0]/c[1]") */
repeaterPath: string;
/** User-facing name of the lines section (e.g., "Lines", "Sales Lines") */
caption: string;
/** Line item records */
lines: PageRecord[];
/** Total number of lines */
totalCount: number;
}
/**
* Result of Document page data extraction (header + lines).
*/
export interface DocumentPageDataExtractionResult extends PageDataExtractionResult {
pageType: 'document';
/** Header record (card-like data) */
header: PageRecord;
/** Lines blocks (one or more repeaters with line data) */
linesBlocks: DocumentLinesBlock[];
/** For backwards compatibility, records[0] contains the header */
records: [PageRecord];
}
/**
* Column metadata extracted from LogicalForm.Columns[] for a single runtime cell ID.
*/
export interface ColumnMapping {
/** Runtime cell ID key used in DataRowUpdated/DataRowInserted (e.g., "1295001522_c1", "140"). */
runtimeId: string;
/** Semantic field name: prefer Caption, then DesignName, then Name. */
semanticName: string;
/** User-facing caption, if present. */
caption?: string | null;
/** DesignName from LogicalForm, if present. */
designName?: string | null;
/** Numeric control ID, when available (Composite pattern). */
controlId?: number | null;
/** BC table field number, when available. */
tableFieldNo?: number | null;
/** Column index within the repeater, if present. */
columnIndex?: number | null;
}
/**
* Extracts data records from BC pages.
*/
export declare class PageDataExtractor {
/**
* Determines if a LogicalForm represents a list page.
*
* Uses CacheKey and ViewMode to distinguish:
* - List pages: typically have ViewMode other than 2 (View/Edit mode)
* - Card pages: ViewMode 2 (shows single record)
*
* Note: Some card pages have embedded repeaters (for line items),
* so we can't rely solely on the presence of repeater controls.
*/
isListPage(logicalForm: LogicalForm): boolean;
/**
* Checks if LogicalForm has a repeater control at the top level
* (not nested in tabs/parts).
*/
private hasTopLevelRepeater;
/**
* Finds all repeater controls in a LogicalForm tree with their control paths.
* Used for Document pages to identify line item sections.
*
* @param logicalForm - The BC LogicalForm to search
* @returns Array of repeater info with path, caption, and DesignName
*/
findAllRepeatersWithPaths(logicalForm: LogicalForm): Array<{
path: string;
caption: string;
designName: string;
controlType: 'rc' | 'lrc';
}>;
/**
* Extracts data from a card page (single record).
*
* IMPORTANT: BC uses different patterns depending on how the page was opened:
* - OpenForm (get_page_metadata): Values in control.StringValue/ObjectValue
* - InvokeAction (drill-down): Values in DataRowUpdated.cells (list pattern)
*
* This method handles both patterns automatically.
*/
extractCardPageData(logicalForm: LogicalForm, handlers?: readonly unknown[]): Result<PageDataExtractionResult, BCError>;
/**
* Extracts data from a list page (multiple records).
* Data arrives via DataRefreshChange handlers (async).
*
* @param handlers - Handlers containing DataRefreshChange with row data
* @param logicalForm - Optional LogicalForm metadata to filter visible fields only
*/
extractListPageData(handlers: readonly unknown[], logicalForm?: LogicalForm): Result<PageDataExtractionResult, BCError>;
/**
* Extracts data from a Document page (header + lines).
*
* Document pages (Sales Orders, Purchase Orders) have:
* - Header fields (card-like) - extracted from PropertyChanges
* - Line sections (list-like repeaters) - extracted from DataRefreshChange
*
* @param logicalForm - The BC LogicalForm
* @param handlers - All handlers from OpenForm + LoadForm
* @returns Document page extraction result with header and linesBlocks
*/
extractDocumentPageData(logicalForm: LogicalForm, handlers: readonly unknown[]): Result<DocumentPageDataExtractionResult, BCError>;
/**
* Checks if control type is a field control.
*/
private isFieldControl;
/**
* Checks if control type is a repeater control.
*/
private isRepeaterControl;
/**
* Walks control tree and calls visitor for each control.
*/
private walkControls;
/**
* Gets the field name from a control.
* Prefers DesignName → Name → Caption (only as last resort).
* Caption should only be used when we have no better identifier to avoid mixing field values.
*/
private getFieldName;
/**
* Extracts field value from a LogicalForm control (card page pattern).
*
* IMPORTANT: BC sends values in different locations depending on the operation:
* - OpenForm (get_page_metadata): Values in control.StringValue/ObjectValue
* - InvokeAction (drill-down): Values in control.Properties.Value
*
* This method checks BOTH locations to support both scenarios.
*/
private extractFieldValueFromControl;
/**
* Extracts value from a select/enum control.
*/
private extractSelectValue;
/**
* Extracts a record from a DataRowInserted row (list page pattern).
*
* @param rowData - Row data with cells object
* @param fieldMetadata - Optional field metadata for visibility filtering
* @param columnMappings - Optional column mappings for runtime ID → semantic name translation
*/
private extractRecordFromRow;
/**
* Extracts flat cells from ClientDataRow (skipping known metadata fields).
*/
private extractFlatCells;
/**
* Extracts field value from a cell (DataRefreshChange pattern).
*/
private extractCellValue;
/**
* Finds DataRowUpdated from handlers (drill-down pattern).
*/
private findDataRowUpdated;
/**
* Builds a field metadata map from LogicalForm for visibility filtering.
*
* Implements Visibility & Relevance Heuristic:
* - Checks Visible property (false = hidden field)
* - Checks Caption presence (no caption = likely internal anchor)
* - Prioritizes Field controls over Group/Container controls
*/
private buildFieldMetadataMap;
/**
* Extracts runtime cell ID → column metadata mappings from LogicalForm repeater Columns[].
*
* Supports both:
* - Composite IDs: ColumnBinder.Name = "{controlId}_c{tableFieldNo}"
* - Simple IDs: Name = "{tableFieldNo}" or symbolic names ("Icon", "Name")
*
* Returns a Map keyed by runtimeId used in DataRowUpdated/DataRowInserted.cells.
*/
private extractColumnMappings;
/**
* Extracts column mappings from Card page field controls' ColumnBinder.Name properties.
* Unlike List pages (which use repeater Columns), Card pages have ColumnBinder on individual field controls.
*
* @param logicalForm - The Card page LogicalForm
* @returns Map of runtime ID (from ColumnBinder.Name) to semantic field name
*/
private extractCardControlColumnMappings;
/**
* Extracts card data from DataRowUpdated (drill-down pattern).
* Uses the same cell extraction and field mapping logic as list pages.
*/
private extractFromDataRowUpdated;
/**
* Applies PropertyChanges from handlers to a deep-cloned LogicalForm.
* PropertyChanges contain field values that arrive after the initial LogicalForm.
*
* @param logicalForm - Original LogicalForm (will be cloned, not mutated)
* @param handlers - Handlers potentially containing PropertyChanges
* @returns Object with cloned+updated form and count of applied changes
*/
applyPropertyChangesToLogicalForm(logicalForm: LogicalForm, handlers: readonly unknown[]): {
updatedForm: LogicalForm;
appliedCount: number;
};
/**
* Recursively finds the first field control within a control tree.
* Used to redirect PropertyChanges from group controls to actual field controls.
*/
private findFirstFieldControl;
/**
* Applies a single PropertyChanges object to a LogicalForm.
* Resolves the target control by controlPath and sets Properties.Value.
*
* @param form - LogicalForm to update (mutated in-place)
* @param propertyChange - PropertyChanges object containing Changes
* @returns Number of properties applied (0 or 1)
*/
private applyPropertyChange;
/**
* Resolves a control by its controlPath (e.g., "server:c[0]/c[1]").
* Traverses the Children array using c[index] segments.
*
* @param form - LogicalForm containing the control tree
* @param controlPath - Path like "server:c[0]/c[1]"
* @returns Control if found, null otherwise
*/
private resolveControlByPath;
}
//# sourceMappingURL=page-data-extractor.d.ts.map