UNPKG

aspose.cells.mcp

Version:

Excel MCP - AI-powered Excel automation server for Excel AI, Excel Formula MCP, spreadsheets MCP, and Excel automation using Aspose.Cells for Node.js

568 lines (516 loc) 20.9 kB
/** * Write Worksheet Service - Simplified version */ import fs from 'fs'; import { getAspose, getAbsolutePath, handleExcelFileError, ensureDirectoryExists, columnLetterToIndex } from '../core/utils.js'; import { stringToEnum, hexToColor } from '../core/enum-converter.js'; interface CellContent { value?: any; text?: string; formula?: string; dataType?: 'string' | 'numeric' | 'boolean' | 'datetime'; } interface CellArea { range: string; type: 'single' | 'merged'; content?: CellContent; styleId?: number; } interface FontFormat { name?: string; size?: number; color?: string; bold?: boolean; italic?: boolean; strikethrough?: boolean; underline?: string; } interface AlignmentFormat { horizontal?: string; vertical?: string; textRotation?: number; indent?: number; } interface BackgroundFormat { pattern?: string; color?: string; } interface BorderFormat { style?: string; color?: string; } interface BordersFormat { top?: BorderFormat; bottom?: BorderFormat; left?: BorderFormat; right?: BorderFormat; } interface StyleFormat { styleId?: number; font?: FontFormat; alignment?: AlignmentFormat; background?: BackgroundFormat; borders?: BordersFormat; numberFormat?: string | number; } interface WorksheetDimensions { totalRows?: number; totalColumns?: number; } interface WorksheetInfo { name?: string; index?: number; usedRange?: string; dimensions?: WorksheetDimensions; rowHeights?: Record<string, number>; columnWidths?: Record<string, number>; } interface WriteWorksheetArgs { filepath: string; worksheetInfo?: WorksheetInfo; styles?: StyleFormat[]; cellsAreas: CellArea[]; overwrite?: boolean; } interface UpdateWorksheetArgs { filepath: string; worksheetInfo?: WorksheetInfo; styles?: StyleFormat[]; cellsAreas: CellArea[]; } interface ServiceResponse { content: Array<{ type: 'text'; text: string; }>; [key: string]: unknown; } interface ProcessResult { worksheetName: string; processedCells: number; mergedAreas: number; stylesUsed: number; } interface CellCoordinates { startRow: number; startCol: number; endRow: number; endCol: number; } interface RangeInfo { startCell: string; endCell: string; isSingleCell: boolean; } /** * Common worksheet processing logic * @param workbook - Aspose workbook instance * @param AsposeCells - Aspose cells module * @param args - Service arguments * @returns Processing result */ function processWorksheetData(workbook: any, AsposeCells: any, args: WriteWorksheetArgs | UpdateWorksheetArgs): ProcessResult { const worksheet = workbook.getWorksheets().get(0); const worksheetName = args.worksheetInfo?.name || 'Sheet1'; worksheet.setName(worksheetName); if (args.worksheetInfo) setDimensions(worksheet, args.worksheetInfo); const result = processCellAreas(worksheet, AsposeCells, workbook, args.cellsAreas || [], args.styles || []); return { worksheetName, processedCells: result.processedCells, mergedAreas: result.mergedAreas, stylesUsed: result.stylesUsed, }; } export const writeWorksheetService = { definition: { name: 'write_worksheet', description: 'Create Excel worksheet from JSON format(Includes styles fonts formulas and more).', inputSchema: { type: 'object', properties: { filepath: { type: 'string', description: 'Output Excel file path' }, worksheetInfo: { type: 'object', properties: { name: { type: 'string', description: 'Worksheet name' }, index: { type: 'number', description: 'Worksheet index' }, usedRange: { type: 'string', description: 'Used range (e.g., A1:B10)' }, dimensions: { type: 'object', properties: { totalRows: { type: 'number', description: 'Total number of rows' }, totalColumns: { type: 'number', description: 'Total number of columns' } } }, rowHeights: { type: 'object', description: 'Row heights mapping. Example: {"1": 40, "2": 16, "8": 33}' }, columnWidths: { type: 'object', description: 'Column widths mapping. Example: {"A": 89, "B": 145, "C": 110}' } } }, styles: { type: 'array', description: 'Array of formatting styles', items: { type: 'object', properties: { styleId: { type: 'number', description: 'Style ID' }, font: { type: 'object', properties: { name: { type: 'string', description: 'Font name' }, size: { type: 'number', description: 'Font size' }, color: { type: 'string', description: 'Font color in hex format' }, bold: { type: 'boolean', description: 'Bold formatting' }, italic: { type: 'boolean', description: 'Italic formatting' }, strikethrough: { type: 'boolean', description: 'Strikethrough formatting' }, underline: { type: 'string', description: 'Underline type' } } }, alignment: { type: 'object', properties: { horizontal: { type: 'string', description: 'Horizontal alignment' }, vertical: { type: 'string', description: 'Vertical alignment' } } }, background: { type: 'object', properties: { pattern: { type: 'string', description: 'Background pattern' }, color: { type: 'string', description: 'Background color in hex format' } } }, borders: { type: 'object', properties: { top: { type: 'object', properties: { style: { type: 'string' }, color: { type: 'string' } } }, bottom: { type: 'object', properties: { style: { type: 'string' }, color: { type: 'string' } } }, left: { type: 'object', properties: { style: { type: 'string' }, color: { type: 'string' } } }, right: { type: 'object', properties: { style: { type: 'string' }, color: { type: 'string' } } } } }, numberFormat: { type: ['string', 'number'], description: 'Number format' } } } }, cellsAreas: { type: 'array', description: 'Array of cell areas containing data and formatting', items: { type: 'object', properties: { range: { type: 'string', description: 'Cell range (A1 or A1:B2)' }, type: { type: 'string', enum: ['single', 'merged'], description: 'Cell type' }, content: { type: 'object', properties: { value: { description: 'Cell value' }, text: { type: 'string', description: 'Cell text representation' }, formula: { type: 'string', description: 'Cell formula' }, dataType: { type: 'string', enum: ['string', 'numeric', 'boolean', 'datetime'], description: 'Data type' } } }, styleId: { type: 'number', description: 'Style ID reference' } }, required: ['range', 'type'] } }, overwrite: { type: 'boolean', default: true, description: 'Whether to overwrite existing file' } }, required: ['filepath', 'cellsAreas'] } }, async handler(args: WriteWorksheetArgs): Promise<ServiceResponse> { let fullPath: string = ''; try { const AsposeCells = getAspose(); fullPath = getAbsolutePath(args.filepath); // Handle overwrite parameter if (args.overwrite === false && fs.existsSync(fullPath)) { throw new Error(`File already exists: ${args.filepath}. Set overwrite=true to overwrite existing file.`); } ensureDirectoryExists(fullPath); const workbook = new AsposeCells.Workbook(); const result = processWorksheetData(workbook, AsposeCells, args); workbook.save(fullPath); return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: `Excel file created successfully at ${args.filepath}`, ...result }, null, 2) }] }; } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); handleExcelFileError(err, fullPath || args.filepath, args.filepath, 'writing worksheet'); } } }; export const updateWorksheetService = { definition: { name: 'update_worksheet', description: 'Update existing Excel worksheet with new data from JSON format.', inputSchema: { type: 'object', properties: { filepath: { type: 'string', description: 'Path to existing Excel file' }, worksheetInfo: { type: 'object', properties: { name: { type: 'string', description: 'Worksheet name' }, index: { type: 'number', description: 'Worksheet index' }, usedRange: { type: 'string', description: 'Used range (e.g., A1:B10)' }, dimensions: { type: 'object', properties: { totalRows: { type: 'number', description: 'Total number of rows' }, totalColumns: { type: 'number', description: 'Total number of columns' } } }, rowHeights: { type: 'object', description: 'Row heights mapping. Example: {"1": 40, "2": 16, "8": 33}' }, columnWidths: { type: 'object', description: 'Column widths mapping. Example: {"A": 89, "B": 145, "C": 110}' } } }, styles: { type: 'array', description: 'Array of formatting styles', items: { type: 'object', properties: { styleId: { type: 'number', description: 'Style ID' }, font: { type: 'object', properties: { name: { type: 'string', description: 'Font name' }, size: { type: 'number', description: 'Font size' }, color: { type: 'string', description: 'Font color in hex format' }, bold: { type: 'boolean', description: 'Bold formatting' }, italic: { type: 'boolean', description: 'Italic formatting' }, strikethrough: { type: 'boolean', description: 'Strikethrough formatting' }, underline: { type: 'string', description: 'Underline type' } } }, alignment: { type: 'object', properties: { horizontal: { type: 'string', description: 'Horizontal alignment' }, vertical: { type: 'string', description: 'Vertical alignment' } } }, background: { type: 'object', properties: { pattern: { type: 'string', description: 'Background pattern' }, color: { type: 'string', description: 'Background color in hex format' } } }, borders: { type: 'object', properties: { top: { type: 'object', properties: { style: { type: 'string' }, color: { type: 'string' } } }, bottom: { type: 'object', properties: { style: { type: 'string' }, color: { type: 'string' } } }, left: { type: 'object', properties: { style: { type: 'string' }, color: { type: 'string' } } }, right: { type: 'object', properties: { style: { type: 'string' }, color: { type: 'string' } } } } }, numberFormat: { type: ['string', 'number'], description: 'Number format' } } } }, cellsAreas: { type: 'array', description: 'Array of cell areas containing data and formatting', items: { type: 'object', properties: { range: { type: 'string', description: 'Cell range (A1 or A1:B2)' }, type: { type: 'string', enum: ['single', 'merged'], description: 'Cell type' }, content: { type: 'object', properties: { value: { description: 'Cell value' }, text: { type: 'string', description: 'Cell text representation' }, formula: { type: 'string', description: 'Cell formula' }, dataType: { type: 'string', enum: ['string', 'numeric', 'boolean', 'datetime'], description: 'Data type' } } }, styleId: { type: 'number', description: 'Style ID reference' } }, required: ['range', 'type'] } } }, required: ['filepath', 'cellsAreas'] } }, async handler(args: UpdateWorksheetArgs): Promise<ServiceResponse> { let fullPath: string = ''; try { const AsposeCells = getAspose(); fullPath = getAbsolutePath(args.filepath); // Check if file exists if (!fs.existsSync(fullPath)) { throw new Error(`File not found: ${args.filepath}. Use write_worksheet to create a new file.`); } const workbook = new AsposeCells.Workbook(fullPath); const result = processWorksheetData(workbook, AsposeCells, args); workbook.save(fullPath); return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: `Excel file updated successfully at ${args.filepath}`, ...result }, null, 2) }] }; } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); handleExcelFileError(err, fullPath || args.filepath, args.filepath, 'updating worksheet'); } } }; function setDimensions(worksheet: any, worksheetInfo: WorksheetInfo): void { const cells = worksheet.getCells(); if (worksheetInfo.rowHeights) { for (const [rowNum, height] of Object.entries(worksheetInfo.rowHeights)) { cells.setRowHeightPixel(parseInt(rowNum) - 1, height); } } if (worksheetInfo.columnWidths) { for (const [colLetter, width] of Object.entries(worksheetInfo.columnWidths)) { cells.setColumnWidthPixel(columnLetterToIndex(colLetter), width); } } } function processCellAreas(worksheet: any, AsposeCells: any, workbook: any, cellsAreas: CellArea[], styles: StyleFormat[]): { processedCells: number; mergedAreas: number; stylesUsed: number } { const cells = worksheet.getCells(); let mergedAreas = 0, processedCells = 0, stylesUsed = 0; for (const cellArea of cellsAreas) { const range = parseRange(cellArea.range); const coords = getCellCoordinates(cells, range.startCell, range.endCell); if (cellArea.type === 'merged' && !range.isSingleCell) { cells.merge(coords.startRow, coords.startCol, coords.endRow - coords.startRow + 1, coords.endCol - coords.startCol + 1); mergedAreas++; } if (cellArea.content) setCellContent(cells.get(coords.startRow, coords.startCol), cellArea.content); if (cellArea.styleId !== null && cellArea.styleId !== undefined && styles[cellArea.styleId]) { applyCellFormat(cells, AsposeCells, workbook, coords, styles[cellArea.styleId]); stylesUsed++; } processedCells++; } return { processedCells, mergedAreas, stylesUsed }; } function parseRange(range: string): RangeInfo { const [startCell, endCell = startCell] = range.split(':'); return { startCell: startCell.trim(), endCell: endCell.trim(), isSingleCell: startCell === endCell }; } function getCellCoordinates(cells: any, startCellRef: string, endCellRef: string): CellCoordinates { const startCell = cells.get(startCellRef); const endCell = cells.get(endCellRef); return { startRow: startCell.getRow(), startCol: startCell.getColumn(), endRow: endCell.getRow(), endCol: endCell.getColumn(), }; } function setCellContent(cell: any, content: CellContent): void { if (content.formula) { cell.setFormula(content.formula); } else if (content.value !== null && content.value !== undefined) { cell.setValue(content.value); } else if (content.text) { cell.setValue(content.text); } } function applyCellFormat(cells: any, AsposeCells: any, workbook: any, coords: CellCoordinates, format: StyleFormat): void { const style = workbook.createStyle(); const styleFlag = new AsposeCells.StyleFlag(); if (format.font) { applyFontFormat(style.getFont(), AsposeCells, format.font); styleFlag.setFont(true); } if (format.alignment) { applyAlignmentFormat(style, AsposeCells, format.alignment); styleFlag.setHorizontalAlignment(true); styleFlag.setVerticalAlignment(true); } if (format.background) { applyBackgroundFormat(style, AsposeCells, format.background); styleFlag.setCellShading(true); } if (format.borders) { applyBorderFormat(style, AsposeCells, format.borders); styleFlag.setBorders(true); } if (format.numberFormat) { if (typeof format.numberFormat === 'string') { style.setCustom(format.numberFormat); } else { style.setNumber(format.numberFormat); } styleFlag.setNumberFormat(true); } const range = cells.createRange(coords.startRow, coords.startCol, coords.endRow - coords.startRow + 1, coords.endCol - coords.startCol + 1); range.applyStyle(style, styleFlag); } function applyFontFormat(font: any, AsposeCells: any, fontFormat: FontFormat): void { if (fontFormat.name) font.setName(fontFormat.name); if (fontFormat.size) font.setSize(fontFormat.size); if (fontFormat.bold) font.setIsBold(fontFormat.bold); if (fontFormat.italic) font.setIsItalic(fontFormat.italic); if (fontFormat.strikethrough) font.setIsStrikeout(fontFormat.strikethrough); if (fontFormat.underline) { const underlineType = stringToEnum(fontFormat.underline, AsposeCells.FontUnderlineType); if (underlineType) font.setUnderline(underlineType); } if (fontFormat.color) { const color = hexToColor(fontFormat.color, AsposeCells); if (color) font.setColor(color); } } function applyAlignmentFormat(style: any, AsposeCells: any, alignmentFormat: AlignmentFormat): void { if (alignmentFormat.horizontal) { const hAlign = stringToEnum(alignmentFormat.horizontal, AsposeCells.TextAlignmentType); if (hAlign) style.setHorizontalAlignment(hAlign); } if (alignmentFormat.vertical) { const vAlign = stringToEnum(alignmentFormat.vertical, AsposeCells.TextAlignmentType); if (vAlign) style.setVerticalAlignment(vAlign); } } function applyBackgroundFormat(style: any, AsposeCells: any, backgroundFormat: BackgroundFormat): void { if (backgroundFormat.pattern) { const pattern = stringToEnum(backgroundFormat.pattern, AsposeCells.BackgroundType); if (pattern) style.setPattern(pattern); } if (backgroundFormat.color) { const color = hexToColor(backgroundFormat.color, AsposeCells); if (color) style.setForegroundColor(color); } } function applyBorderFormat(style: any, AsposeCells: any, bordersFormat: BordersFormat): void { const borderMap: Record<string, any> = { top: AsposeCells.BorderType.TopBorder, bottom: AsposeCells.BorderType.BottomBorder, left: AsposeCells.BorderType.LeftBorder, right: AsposeCells.BorderType.RightBorder }; for (const [position, borderFormat] of Object.entries(bordersFormat)) { if (borderMap[position] && borderFormat?.style) { const borderStyle = stringToEnum(borderFormat.style, AsposeCells.CellBorderType); if (borderStyle) { const border = style.getBorders().get(borderMap[position]); border.setLineStyle(borderStyle); if (borderFormat.color) { const color = hexToColor(borderFormat.color, AsposeCells); if (color) border.setColor(color); } } } } }