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
303 lines • 11.4 kB
JavaScript
/**
* Intelligent Metadata Parser
*
* Reduces BC response from ~729KB to ~15-20KB by:
* 1. Filtering non-essential fields (system fields, hidden controls)
* 2. Summarizing actions into simple enabled/disabled lists
* 3. Extracting semantic meaning and key information
* 4. Removing redundant metadata
*
* Goal: Give LLMs only the actionable, semantic data they need.
*/
import { ok, andThen } from '../core/result.js';
import { PageMetadataParser } from './page-metadata-parser.js';
// ============================================================================
// System Field Patterns to Filter Out
// ============================================================================
/** Fields that are system-managed and rarely needed by LLMs */
const SYSTEM_FIELD_PATTERNS = [
/^SystemId$/i,
/^SystemCreatedAt$/i,
/^SystemCreatedBy$/i,
/^SystemModifiedAt$/i,
/^SystemModifiedBy$/i,
/^timestamp$/i,
/^Last Date Modified$/i,
/^Last Modified Date Time$/i,
/^Id$/i,
/GUID$/i,
];
/** Control types to exclude (internal/system controls) */
const EXCLUDED_CONTROL_TYPES = new Set([
'fhc', // FormHostControl
'stackc', // StackLogicalControl
'stackgc', // StackGroupLogicalControl
'gc', // GroupControl (layout only)
'ssc', // StaticStringControl (labels)
]);
// ============================================================================
// Page Type to Capability Mapping
// ============================================================================
/** Maps page types to their typical capabilities */
const PAGE_CAPABILITIES = {
'Card': ['read', 'update', 'create', 'delete'],
'List': ['read', 'browse', 'filter', 'sort'],
'Document': ['read', 'update', 'post', 'print'],
'Worksheet': ['read', 'update', 'calculate'],
'ListPlus': ['read', 'browse', 'drill-down'],
};
// ============================================================================
// Intelligent Metadata Parser
// ============================================================================
/**
* Parses BC metadata and optimizes it for LLM consumption.
* Dramatically reduces size while preserving semantic meaning.
*/
export class IntelligentMetadataParser {
baseParser;
constructor(baseParser = new PageMetadataParser()) {
this.baseParser = baseParser;
}
/**
* Parses and optimizes page metadata.
*/
parse(handlers) {
// Use base parser to extract raw metadata
const rawResult = this.baseParser.parse(handlers);
// Extract ViewMode and FormStyle from LogicalForm for accurate page type detection
const logicalForm = this.extractLogicalFormFromHandlers(handlers);
const viewMode = logicalForm?.ViewMode;
const formStyle = logicalForm?.FormStyle;
// Transform to optimized format
return andThen(rawResult, raw => {
const optimized = {
pageId: raw.pageId,
title: raw.caption,
summary: this.generateSummary(raw, viewMode, formStyle),
fields: this.optimizeFields(raw.fields),
actions: this.optimizeActions(raw.actions),
stats: {
totalFields: raw.fields.length,
visibleFields: this.optimizeFields(raw.fields).length,
totalActions: raw.actions.length,
enabledActions: raw.actions.filter(a => a.enabled).length,
},
};
return ok(optimized);
});
}
// ============================================================================
// Field Optimization
// ============================================================================
/**
* Filters and optimizes fields for LLM consumption.
* Removes ONLY fields the user cannot see or interact with:
* - System fields (SystemId, timestamps, etc.)
* - Hidden/disabled fields (enabled=false)
* - Internal controls (groups, containers, layout)
*
* Keeps ALL fields a user can see in the BC UI.
* Agent must have same capabilities as human user.
*/
optimizeFields(fields) {
return fields
.filter(field => this.isEssentialField(field))
.map(field => this.toOptimizedField(field));
// NO arbitrary limit - keep all visible fields
}
/**
* Determines if a field is essential (not system/hidden).
*/
isEssentialField(field) {
// Must have a name
if (!field.name && !field.caption) {
return false;
}
const fieldName = field.name || field.caption || '';
// Filter out system fields
if (SYSTEM_FIELD_PATTERNS.some(pattern => pattern.test(fieldName))) {
return false;
}
// Filter out internal control types
if (EXCLUDED_CONTROL_TYPES.has(field.type)) {
return false;
}
// Filter out disabled/hidden fields (usually not relevant)
if (!field.enabled) {
return false;
}
return true;
}
/**
* Converts raw field metadata to optimized format.
*/
toOptimizedField(field) {
const baseOptimized = {
name: field.caption || field.name || 'Unnamed',
type: this.simplifyFieldType(field.type),
editable: field.enabled && !field.readonly,
};
// Include options for selection fields
if (field.options && field.options.length > 0 && field.options.length <= 20) {
return {
...baseOptimized,
options: field.options,
};
}
return baseOptimized;
}
/**
* Converts BC control type to user-friendly type.
*/
simplifyFieldType(controlType) {
const typeMap = {
'sc': 'text',
'dc': 'number',
'bc': 'boolean',
'i32c': 'number',
'i64c': 'number',
'sec': 'option',
'dtc': 'date',
'pc': 'number',
'nuc': 'number',
'ic': 'number',
'chc': 'text',
};
return typeMap[controlType] || 'text';
}
// ============================================================================
// Action Optimization
// ============================================================================
/**
* Simplifies actions into enabled/disabled lists.
* Much more concise than full action metadata.
* Keeps ALL actions visible to user (no arbitrary limits).
*/
optimizeActions(actions) {
const enabled = [];
const disabled = [];
for (const action of actions) {
const name = action.caption || 'Unnamed';
// Skip internal/empty actions
if (!name || name.trim() === '') {
continue;
}
if (action.enabled) {
enabled.push(name);
}
else {
disabled.push(name);
}
}
return {
enabled, // All enabled actions user can see
disabled, // All disabled actions (for context)
};
}
// ============================================================================
// Semantic Summary Generation
// ============================================================================
/** LogicalForm structure from FormToShow handler */
static asLogicalForm(obj) {
if (obj && typeof obj === 'object') {
return obj;
}
return null;
}
/**
* Extracts LogicalForm from handlers (finds FormToShow handler).
*/
extractLogicalFormFromHandlers(handlers) {
for (const handler of handlers) {
if (handler.handlerType === 'DN.LogicalClientEventRaisingHandler' &&
handler.parameters?.[0] === 'FormToShow') {
return IntelligentMetadataParser.asLogicalForm(handler.parameters[1]); // LogicalForm object
}
}
return null;
}
/**
* Generates semantic summary of the page.
* Helps LLMs understand the page's purpose and capabilities.
*/
generateSummary(metadata, viewMode, formStyle) {
// Infer page type using ViewMode/FormStyle (accurate) or fallback to heuristics
const pageType = this.inferPageType(metadata.caption, metadata.pageId, viewMode, formStyle);
// Get capabilities for this page type
const capabilities = PAGE_CAPABILITIES[pageType] || ['read', 'update'];
// Identify key fields (first 5 editable fields)
const keyFields = metadata.fields
.filter(f => f.enabled && !f.readonly)
.slice(0, 5)
.map(f => f.caption || f.name || '')
.filter(name => name !== '');
return {
purpose: this.generatePurpose(metadata.caption, pageType),
capabilities,
keyFields,
};
}
/**
* Infers page type from BC metadata.
*
* Uses ViewMode and FormStyle from LogicalForm (most accurate).
* Falls back to caption/ID heuristics if metadata unavailable.
*
* ViewMode values:
* - 1 = List/Worksheet (multiple records)
* - 2 = Card/Document (single record)
*
* FormStyle values (when ViewMode=2):
* - 1 = Document
* - undefined/absent = Card
*/
inferPageType(caption, pageId, viewMode, formStyle) {
// Primary detection: Use BC's ViewMode and FormStyle metadata
if (viewMode !== undefined) {
if (viewMode === 1) {
// List-style pages (multiple records)
// Both List and Worksheet have ViewMode=1
// Use caption to differentiate if needed
const lower = caption.toLowerCase();
if (lower.includes('worksheet') || lower.includes('journal')) {
return 'Worksheet';
}
return 'List';
}
if (viewMode === 2) {
// Detail-style pages (single record)
// FormStyle differentiates Document from Card
if (formStyle === 1) {
return 'Document';
}
return 'Card';
}
}
// Fallback: Heuristic detection (less reliable)
const lower = caption.toLowerCase();
if (lower.includes('card'))
return 'Card';
if (lower.includes('list'))
return 'List';
if (lower.includes('document') || lower.includes('order') || lower.includes('invoice'))
return 'Document';
if (lower.includes('worksheet') || lower.includes('journal'))
return 'Worksheet';
// Default fallback
return 'Card';
}
/**
* Generates a purpose description.
*/
generatePurpose(caption, pageType) {
const verbMap = {
'Card': 'View and edit',
'List': 'Browse and select',
'Document': 'Create and process',
'Worksheet': 'Enter and calculate',
};
const verb = verbMap[pageType] || 'Manage';
return `${verb} ${caption.replace(/(Card|List|Document|Worksheet)/gi, '').trim()}`;
}
}
//# sourceMappingURL=intelligent-metadata-parser.js.map