bc-symbols-mcp
Version:
MCP server for analyzing Business Central .app files and BC object structures
192 lines • 7.06 kB
JavaScript
import { pipeline } from 'stream';
import { promisify } from 'util';
const asyncPipeline = promisify(pipeline);
export class StreamingSymbolParser {
symbolIndex;
rawData;
constructor() {
this.symbolIndex = {
runtimeVersion: '',
objectIndex: new Map(),
totalObjects: 0,
loadedObjects: 0
};
this.rawData = Buffer.alloc(0);
}
/**
* Parse symbols with progressive loading - builds index first, loads objects on demand
*/
async parseSymbolsProgressive(symbolsBuffer) {
this.rawData = symbolsBuffer;
// Phase 1: Build object index by streaming through the JSON
await this.buildObjectIndex(symbolsBuffer);
return this.symbolIndex;
}
/**
* Build a lightweight index of all objects without loading their full data
*/
async buildObjectIndex(buffer) {
try {
// For now, use simple JSON parsing - streaming implementation can be enhanced later
const jsonString = buffer.toString('utf8');
const data = JSON.parse(jsonString);
this.symbolIndex.runtimeVersion = data.RuntimeVersion || '';
// Process different object types
const objectTypes = ['Tables', 'Codeunits', 'Pages', 'PageExtensions', 'Reports', 'XmlPorts', 'Queries', 'EnumTypes'];
for (const arrayKey of objectTypes) {
if (data[arrayKey] && Array.isArray(data[arrayKey])) {
const objectType = this.getObjectTypeFromPath([], arrayKey);
for (const obj of data[arrayKey]) {
const metadata = this.createObjectMetadata(obj, objectType, 'Root');
if (metadata) {
const indexKey = `${objectType}:${metadata.objectId}:${metadata.name}`;
this.symbolIndex.objectIndex.set(indexKey, metadata);
this.symbolIndex.totalObjects++;
}
}
}
}
}
catch (error) {
console.warn('Error building object index:', error);
// Fallback to empty index
this.symbolIndex.runtimeVersion = '';
this.symbolIndex.totalObjects = 0;
}
}
/**
* Load specific object on demand
*/
async loadObject(objectType, objectId, objectName) {
const indexKey = `${objectType}:${objectId}:${objectName}`;
const metadata = this.symbolIndex.objectIndex.get(indexKey);
if (!metadata) {
throw new Error(`Object not found: ${indexKey}`);
}
if (metadata.loaded) {
return metadata; // Return cached object
}
// Parse specific object from raw buffer using offset/size
const objectData = await this.parseObjectAtOffset(metadata.offset, metadata.size);
metadata.loaded = true;
this.symbolIndex.loadedObjects++;
return objectData;
}
/**
* Load objects of a specific type on demand
*/
async loadObjectsByType(objectType) {
const objects = [];
for (const [key, metadata] of this.symbolIndex.objectIndex) {
if (metadata.objectType === objectType) {
const object = await this.loadObject(metadata.objectType, metadata.objectId, metadata.name);
objects.push(object);
}
}
return objects;
}
/**
* Get object metadata without loading full object
*/
getObjectMetadata(objectType, objectId, objectName) {
const results = [];
for (const metadata of this.symbolIndex.objectIndex.values()) {
if (metadata.objectType === objectType) {
if (objectId !== undefined && metadata.objectId !== objectId)
continue;
if (objectName !== undefined && metadata.name !== objectName)
continue;
results.push(metadata);
}
}
return results;
}
/**
* Get loading statistics
*/
getLoadingStats() {
const percentage = this.symbolIndex.totalObjects > 0
? (this.symbolIndex.loadedObjects / this.symbolIndex.totalObjects) * 100
: 0;
return {
total: this.symbolIndex.totalObjects,
loaded: this.symbolIndex.loadedObjects,
percentage: Math.round(percentage * 100) / 100
};
}
/**
* Create minimal compatible symbol reference for existing code
*/
createLazySymbolReference() {
return {
runtimeVersion: this.symbolIndex.runtimeVersion,
namespaces: [this.createLazyNamespace()]
};
}
createLazyNamespace() {
return {
name: 'Root',
namespaces: [],
tables: [],
codeunits: [],
pages: [],
pageExtensions: [],
reports: [],
xmlPorts: [],
queries: [],
controlAddIns: [],
enumTypes: [],
dotNetPackages: [],
interfaces: [],
permissionSets: [],
permissionSetExtensions: [],
reportExtensions: []
};
}
isObjectArray(path, key) {
const objectArrays = [
'Tables', 'Codeunits', 'Pages', 'PageExtensions', 'Reports',
'XmlPorts', 'Queries', 'EnumTypes', 'ControlAddIns', 'Interfaces',
'PermissionSets', 'PermissionSetExtensions', 'ReportExtensions'
];
return objectArrays.includes(key);
}
getObjectTypeFromPath(path, key) {
const typeMap = {
'Tables': 'table',
'Codeunits': 'codeunit',
'Pages': 'page',
'PageExtensions': 'pageextension',
'Reports': 'report',
'XmlPorts': 'xmlport',
'Queries': 'query',
'EnumTypes': 'enum',
'ControlAddIns': 'controladdin',
'Interfaces': 'interface',
'PermissionSets': 'permissionset',
'PermissionSetExtensions': 'permissionsetextension',
'ReportExtensions': 'reportextension'
};
return typeMap[key] || 'unknown';
}
createObjectMetadata(value, objectType, namespace) {
if (!value || typeof value !== 'object')
return null;
return {
objectType,
objectId: value.Id || 0,
name: value.Name || '',
namespace,
offset: 0, // Will be calculated during actual parsing
size: 0, // Will be calculated during actual parsing
loaded: false
};
}
async parseObjectAtOffset(offset, size) {
// Implementation would extract and parse specific JSON section
// For now, return placeholder - full implementation would use
// buffer slicing and targeted JSON parsing
return {};
}
}
//# sourceMappingURL=streaming-parser.js.map