MCP Excel Reader
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import * as XLSX from 'xlsx';
import { existsSync, readFileSync } from 'fs';
interface ExcelChunk {
rowStart: number;
rowEnd: number;
columns: string[];
data: Record<string, any>[];
}
interface ExcelSheetData {
name: string;
totalRows: number;
totalColumns: number;
chunk: ExcelChunk;
hasMore: boolean;
nextChunk?: {
rowStart: number;
columns: string[];
};
}
interface ExcelData {
fileName: string;
totalSheets: number;
currentSheet: ExcelSheetData;
}
interface ReadExcelArgs {
filePath: string;
sheetName?: string;
startRow?: number;
maxRows?: number;
}
const MAX_RESPONSE_SIZE = 100 * 1024; // 100KB default max response size
const isValidReadExcelArgs = (args: any): args is ReadExcelArgs =>
typeof args === 'object' &&
args !== null &&
typeof args.filePath === 'string' &&
(args.sheetName === undefined || typeof args.sheetName === 'string') &&
(args.startRow === undefined || typeof args.startRow === 'number') &&
(args.maxRows === undefined || typeof args.maxRows === 'number');
// Estimate size of stringified JSON
const estimateJsonSize = (obj: any): number => {
const str = JSON.stringify(obj);
return str.length * 2; // Rough estimate, multiply by 2 for unicode
};
// Calculate optimal chunk size
const calculateChunkSize = (data: any[], maxSize: number): number => {
const singleRowSize = estimateJsonSize(data[0]);
return Math.max(1, Math.floor(maxSize / singleRowSize));
};
class ExcelReaderServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: 'excel-reader',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private readExcelFile(args: ReadExcelArgs): ExcelData {
const { filePath, sheetName, startRow = 0, maxRows } = args;
if (!existsSync(filePath)) {
throw new McpError(
ErrorCode.InvalidRequest,
`File not found: ${filePath}`
);
}
try {
// Read file as buffer first
const data = readFileSync(filePath);
const workbook = XLSX.read(data, {
type: 'buffer',
cellDates: true,
cellNF: false,
cellText: false,
dateNF: 'yyyy-mm-dd'
});
const fileName = filePath.split(/[\\/]/).pop() || '';
const selectedSheetName = sheetName || workbook.SheetNames[0];
const worksheet = workbook.Sheets[selectedSheetName];
const allData = XLSX.utils.sheet_to_json(worksheet, {
raw: true,
dateNF: 'yyyy-mm-dd'
}) as Record<string, any>[];
const totalRows = allData.length;
const columns = totalRows > 0 ? Object.keys(allData[0] as object) : [];
const totalColumns = columns.length;
// Calculate chunk size based on data size
let effectiveMaxRows = maxRows;
if (!effectiveMaxRows) {
const initialChunk = allData.slice(0, 100); // Sample first 100 rows
if (initialChunk.length > 0) {
effectiveMaxRows = calculateChunkSize(initialChunk, MAX_RESPONSE_SIZE);
} else {
effectiveMaxRows = 100; // Default if no data
}
}
const endRow = Math.min(startRow + effectiveMaxRows, totalRows);
const chunkData = allData.slice(startRow, endRow);
const hasMore = endRow < totalRows;
const nextChunk = hasMore ? {
rowStart: endRow,
columns
} : undefined;
return {
fileName,
totalSheets: workbook.SheetNames.length,
currentSheet: {
name: selectedSheetName,
totalRows,
totalColumns,
chunk: {
rowStart: startRow,
rowEnd: endRow,
columns,
data: chunkData
},
hasMore,
nextChunk
}
};
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Error reading Excel file: ${error instanceof Error ? error.message : String(error)}`
);
}
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'read_excel',
description: 'Read an Excel file and return its contents as structured data',
inputSchema: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'Path to the Excel file to read',
},
sheetName: {
type: 'string',
description: 'Name of the sheet to read (optional)',
},
startRow: {
type: 'number',
description: 'Starting row index (optional)',
},
maxRows: {
type: 'number',
description: 'Maximum number of rows to read (optional)',
},
},
required: ['filePath'],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== 'read_excel') {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
if (!isValidReadExcelArgs(request.params.arguments)) {
throw new McpError(
ErrorCode.InvalidParams,
'Invalid read_excel arguments'
);
}
try {
const data = this.readExcelFile(request.params.arguments);
return {
content: [
{
type: 'text',
text: JSON.stringify(data, null, 2),
},
],
};
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`Unexpected error: ${error instanceof Error ? error.message : String(error)}`
);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Excel Reader MCP server running on stdio');
}
}
const server = new ExcelReaderServer();
server.run().catch(console.error);