bc-symbols-mcp
Version:
MCP server for analyzing Business Central .app files and BC object structures
264 lines • 10.1 kB
JavaScript
import AdmZip from 'adm-zip';
import { createHash } from 'node:crypto';
import { readFileSync } from 'fs';
export class AppExtractor {
/**
* Extract and process a Business Central .app file
* @param filePath Path to the .app file
* @returns Promise<BCApp> Processed app data
*/
async extractApp(filePath) {
try {
// Read the file and calculate hash
const fileBuffer = readFileSync(filePath);
const fileHash = this.calculateFileHash(fileBuffer);
// Create ZIP instance with the file buffer
// BC .app files have a 40-byte prefix before the actual ZIP data
const zipBuffer = fileBuffer.subarray(40); // Skip the 40-byte prefix
const zip = new AdmZip(zipBuffer);
// Extract required files
const manifestXml = this.extractFileFromZip(zip, 'NavxManifest.xml');
const symbolsJson = this.extractFileFromZip(zip, 'SymbolReference.json');
if (!manifestXml || !symbolsJson) {
throw new Error('Required files (NavxManifest.xml or SymbolReference.json) not found in .app file');
}
// Parse the manifest to get basic app info
const manifest = await this.parseManifest(manifestXml);
// Use streaming parser for large symbol files
const symbolParser = await import('./symbol-parser.js');
const parser = new symbolParser.SymbolParser();
const symbols = await parser.parseSymbolReferenceStreaming(symbolsJson);
// Create BCApp object
const app = {
id: manifest.app.id,
name: manifest.app.name,
publisher: manifest.app.publisher,
version: manifest.app.version,
description: manifest.app.description,
brief: manifest.app.brief,
platform: manifest.app.platform,
application: manifest.app.application,
runtime: manifest.app.runtime,
target: manifest.app.target,
dependencies: manifest.dependencies,
idRanges: manifest.idRanges,
filePath,
fileHash,
manifest,
symbols
};
return app;
}
catch (error) {
throw new Error(`Failed to extract app from ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Calculate SHA-256 hash of file buffer
*/
calculateFileHash(buffer) {
return createHash('sha256').update(buffer).digest('hex');
}
/**
* Extract a specific file from the ZIP archive
*/
extractFileFromZip(zip, fileName) {
try {
const entry = zip.getEntry(fileName);
if (!entry) {
return null;
}
return entry.getData();
}
catch (error) {
console.warn(`Failed to extract ${fileName} from ZIP:`, error);
return null;
}
}
/**
* Parse NavxManifest.xml content
*/
async parseManifest(manifestBuffer) {
const xml2js = await import('xml2js');
const parser = new xml2js.Parser({ explicitArray: false });
try {
const result = await parser.parseStringPromise(manifestBuffer.toString('utf8'));
return this.transformManifest(result);
}
catch (error) {
throw new Error(`Failed to parse NavxManifest.xml: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Transform parsed XML manifest to our BCManifest format
*/
transformManifest(xmlData) {
const packageData = xmlData.Package;
const app = packageData.App;
return {
app: {
id: app.$.Id,
name: app.$.Name,
publisher: app.$.Publisher,
brief: app.$.Brief || '',
description: app.$.Description || '',
version: app.$.Version,
compatibilityId: app.$.CompatibilityId || '0.0.0.0',
privacyStatement: app.$.PrivacyStatement || '',
eula: app.$.EULA || '',
help: app.$.Help || '',
helpBaseUrl: app.$.HelpBaseUrl || '',
url: app.$.Url || '',
logo: app.$.Logo || '',
platform: app.$.Platform,
application: app.$.Application,
runtime: app.$.Runtime,
target: app.$.Target,
showMyCode: app.$.ShowMyCode === 'True'
},
idRanges: this.transformIdRanges(packageData.IdRanges),
dependencies: this.transformDependencies(packageData.Dependencies),
internalsVisibleTo: [],
features: this.transformFeatures(packageData.Features),
preprocessorSymbols: [],
suppressWarnings: this.transformSuppressWarnings(packageData.SuppressWarnings),
resourceExposurePolicy: this.transformResourceExposurePolicy(packageData.ResourceExposurePolicy),
keyVaultUrls: [],
build: this.transformBuildInfo(packageData.Build)
};
}
/**
* Transform ID ranges from XML format
*/
transformIdRanges(idRangesData) {
if (!idRangesData || !idRangesData.IdRange) {
return [];
}
const ranges = Array.isArray(idRangesData.IdRange) ? idRangesData.IdRange : [idRangesData.IdRange];
return ranges.map((range) => ({
minObjectId: parseInt(range.$.MinObjectId, 10),
maxObjectId: parseInt(range.$.MaxObjectId, 10)
}));
}
/**
* Transform dependencies from XML format
*/
transformDependencies(dependenciesData) {
if (!dependenciesData || !dependenciesData.Dependency) {
return [];
}
const deps = Array.isArray(dependenciesData.Dependency) ? dependenciesData.Dependency : [dependenciesData.Dependency];
return deps.map((dep) => ({
id: dep.$.Id,
name: dep.$.Name,
publisher: dep.$.Publisher,
minVersion: dep.$.MinVersion,
compatibilityId: dep.$.CompatibilityId || '0.0.0.0'
}));
}
/**
* Transform features from XML format
*/
transformFeatures(featuresData) {
if (!featuresData || !featuresData.Feature) {
return [];
}
const features = Array.isArray(featuresData.Feature) ? featuresData.Feature : [featuresData.Feature];
return features.map((feature) => typeof feature === 'string' ? feature : feature._);
}
/**
* Transform suppress warnings from XML format
*/
transformSuppressWarnings(suppressWarningsData) {
if (!suppressWarningsData || !suppressWarningsData.SuppressWarning) {
return [];
}
const warnings = Array.isArray(suppressWarningsData.SuppressWarning) ? suppressWarningsData.SuppressWarning : [suppressWarningsData.SuppressWarning];
return warnings.map((warning) => ({
name: warning.$.Name
}));
}
/**
* Transform resource exposure policy from XML format
*/
transformResourceExposurePolicy(policyData) {
if (!policyData) {
return {
allowDebugging: false,
allowDownloadingSource: false,
includeSourceInSymbolFile: false,
applyToDevExtension: false
};
}
return {
allowDebugging: policyData.$.AllowDebugging === 'true',
allowDownloadingSource: policyData.$.AllowDownloadingSource === 'true',
includeSourceInSymbolFile: policyData.$.IncludeSourceInSymbolFile === 'true',
applyToDevExtension: policyData.$.ApplyToDevExtension === 'true'
};
}
/**
* Transform build info from XML format
*/
transformBuildInfo(buildData) {
if (!buildData) {
return {
by: '',
timestamp: '',
compilerVersion: ''
};
}
return {
by: buildData.$.By || '',
timestamp: buildData.$.Timestamp || '',
compilerVersion: buildData.$.CompilerVersion || ''
};
}
/**
* Parse SymbolReference.json content
*/
async parseSymbols(symbolsBuffer) {
try {
let symbolsText = symbolsBuffer.toString('utf8');
// Remove UTF-8 BOM if present
if (symbolsText.charCodeAt(0) === 0xFEFF) {
symbolsText = symbolsText.slice(1);
}
return JSON.parse(symbolsText);
}
catch (error) {
throw new Error(`Failed to parse SymbolReference.json: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* List all files in the ZIP archive
*/
listFiles(filePath) {
try {
const fileBuffer = readFileSync(filePath);
const zipBuffer = fileBuffer.subarray(40); // Skip the 40-byte prefix
const zip = new AdmZip(zipBuffer);
const entries = zip.getEntries();
return entries.map(entry => entry.entryName);
}
catch (error) {
throw new Error(`Failed to list files in ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Extract a specific file from the app
*/
extractFile(filePath, targetFile) {
try {
const fileBuffer = readFileSync(filePath);
const zipBuffer = fileBuffer.subarray(40); // Skip the 40-byte prefix
const zip = new AdmZip(zipBuffer);
return this.extractFileFromZip(zip, targetFile);
}
catch (error) {
console.warn(`Failed to extract ${targetFile} from ${filePath}:`, error);
return null;
}
}
}
//# sourceMappingURL=app-extractor.js.map