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
text/typescript
/**
* 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);
}
}
}
}
}