@makeshkumar/mcp-xl-reader 1.0.2 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -55
- package/dist/index.js +171 -2
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,65 +1,23 @@
|
|
|
1
|
-
# XL Reader MCP Server
|
|
2
|
-
|
|
3
|
-
A Model Context Protocol (MCP) server that provides AI assistants with the ability to read, search, and update local `.xlsx` Excel files.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- **Read Spreadsheets**: Stream memory-efficient JSON rows from large Excel workbooks.
|
|
8
|
-
- **Search within Sheets**: Perform case-insensitive fast searches on specific columns or headers.
|
|
9
|
-
- **Update Cells**: Dynamically write string or numeric values back to the local Excel document.
|
|
10
|
-
- **List Sheets**: Easily pull the names of all worksheets in a workbook.
|
|
11
|
-
|
|
12
|
-
## Installation
|
|
13
|
-
|
|
14
|
-
### Method 1: NPX (Recommended for quick use)
|
|
15
|
-
You can directly run this server using `npx` in your MCP client configuration without globally installing it.
|
|
16
|
-
|
|
17
|
-
```json
|
|
18
|
-
{
|
|
19
|
-
"mcpServers": {
|
|
20
|
-
"xl-reader": {
|
|
21
|
-
"command": "npx",
|
|
22
|
-
"args": ["-y", "@makeshkumar/mcp-xl-reader"]
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
```
|
|
27
|
-
*(Replace `@makeshkumar/mcp-xl-reader` with your actual published npm package name once published)*
|
|
28
|
-
|
|
29
|
-
### Method 2: Global Install
|
|
30
|
-
```bash
|
|
31
|
-
npm install -g @makeshkumar/mcp-xl-reader
|
|
32
|
-
mcp-xl-reader
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### Method 3: Local Build
|
|
36
|
-
1. Clone the repository.
|
|
37
|
-
2. Run `npm install` to install dependencies.
|
|
38
|
-
3. Run `npm run build` to compile the TypeScript to JavaScript.
|
|
39
|
-
4. Start the server using `npm start` or point your MCP client to the `dist/index.js` file.
|
|
40
|
-
|
|
41
|
-
## Configuration Details
|
|
42
|
-
|
|
43
|
-
### Security
|
|
44
|
-
By default, the server can access any `.xlsx` file on your system using the absolute path provided by the language model.
|
|
45
|
-
|
|
46
|
-
To restrict access, set the `ALLOWED_DIRECTORIES` environment variable:
|
|
47
|
-
```bash
|
|
48
|
-
export ALLOWED_DIRECTORIES="/Users/yourname/Documents/Spreadsheets,/Users/yourname/Downloads"
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
If defined, the server will strictly reject paths outside these directory trees.
|
|
52
|
-
|
|
53
1
|
## Tools Exposed
|
|
54
2
|
|
|
55
3
|
| Tool Name | Description |
|
|
56
4
|
|-----------|-------------|
|
|
57
5
|
| `list_sheets` | Returns an array of worksheet names available in the workbook. |
|
|
6
|
+
| `find_excel_files` | Recursively finds all .xlsx files in directories. |
|
|
58
7
|
| `read_spreadsheet` | Returns JSON representation of rows. Streams using `worksheet.eachRow` for memory efficiency. |
|
|
59
8
|
| `search_spreadsheet` | Performs a filtered search based on a `columnName` and `queryValue` and returns matching rows. |
|
|
60
9
|
| `update_cell` | Updates the local file and saves changes by setting a new cell value at a specific alphanumeric address (e.g. 'B2'). |
|
|
10
|
+
| `add_worksheet_with_data` | Creates a new worksheet in an existing workbook and populates it with rows. |
|
|
11
|
+
| `create_new_workbook` | Creates a brand new Excel workbook with initial sheet and data. **NEW: Supports optional filePath with reference file directory.** |
|
|
12
|
+
|
|
13
|
+
### create_new_workbook - New Feature
|
|
61
14
|
|
|
62
|
-
|
|
15
|
+
You can now create a new workbook without specifying the full path. Instead, provide a reference file path and the new file will be created in the same directory:
|
|
63
16
|
|
|
64
|
-
1
|
|
65
|
-
|
|
17
|
+
**Option 1: Direct Path (original way)**
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"filePath": "/Users/user/new_file.xlsx",
|
|
21
|
+
"columns": [{"header": "Name", "key": "name"}],
|
|
22
|
+
"rows": [{"name": "John"}]
|
|
23
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,18 @@ async function validateFilePath(filePath) {
|
|
|
42
42
|
}
|
|
43
43
|
return absolutePath;
|
|
44
44
|
}
|
|
45
|
+
// Helper for security check when creating new files (bypasses existence check)
|
|
46
|
+
async function validateNewFilePath(filePath) {
|
|
47
|
+
const absolutePath = path.resolve(filePath);
|
|
48
|
+
const allowedDirectories = process.env.ALLOWED_DIRECTORIES ? process.env.ALLOWED_DIRECTORIES.split(',') : [];
|
|
49
|
+
if (allowedDirectories.length > 0) {
|
|
50
|
+
const isAllowed = allowedDirectories.some(dir => absolutePath.startsWith(path.resolve(dir)));
|
|
51
|
+
if (!isAllowed) {
|
|
52
|
+
throw new Error(`Security Violation: Access to ${absolutePath} is not within allowed directories.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return absolutePath;
|
|
56
|
+
}
|
|
45
57
|
// Helper for security and directory existence check
|
|
46
58
|
async function validateDirectoryPath(directoryPath) {
|
|
47
59
|
const absolutePath = path.resolve(directoryPath);
|
|
@@ -182,6 +194,64 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
182
194
|
},
|
|
183
195
|
required: ["filePath", "cellAddress", "newValue"]
|
|
184
196
|
}
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: "add_worksheet_with_data",
|
|
200
|
+
description: "Creates a new worksheet in an existing workbook and populates it with an array of JSON rows. Great for generating summary reports.",
|
|
201
|
+
inputSchema: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
filePath: { type: "string" },
|
|
205
|
+
sheetName: { type: "string", description: "Name of the new sheet to create." },
|
|
206
|
+
columns: {
|
|
207
|
+
type: "array",
|
|
208
|
+
items: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
header: { type: "string" },
|
|
212
|
+
key: { type: "string" }
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
description: "Array of column definitions, e.g., [{header: 'Name', key: 'name'}]"
|
|
216
|
+
},
|
|
217
|
+
rows: {
|
|
218
|
+
type: "array",
|
|
219
|
+
items: { type: "object" },
|
|
220
|
+
description: "Array of JSON objects representing the rows to insert."
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
required: ["filePath", "sheetName", "columns", "rows"]
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "create_new_workbook",
|
|
228
|
+
description: "Creates a brand new Excel workbook file. If filePath is omitted, it creates the file in the same directory as the reference file.",
|
|
229
|
+
inputSchema: {
|
|
230
|
+
type: "object",
|
|
231
|
+
properties: {
|
|
232
|
+
filePath: { type: "string", description: "Absolute path where the new .xlsx file should be created (optional if referenceFilePath provided)." },
|
|
233
|
+
referenceFilePath: { type: "string", description: "Path to an existing file to use as directory reference (optional)." },
|
|
234
|
+
fileName: { type: "string", description: "Name of the new file (e.g., 'report.xlsx'). Required if referenceFilePath is used." },
|
|
235
|
+
sheetName: { type: "string", description: "Name of the first sheet.", default: "Sheet1" },
|
|
236
|
+
columns: {
|
|
237
|
+
type: "array",
|
|
238
|
+
items: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
header: { type: "string" },
|
|
242
|
+
key: { type: "string" }
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
description: "Array of column definitions, e.g., [{header: 'Name', key: 'name'}]"
|
|
246
|
+
},
|
|
247
|
+
rows: {
|
|
248
|
+
type: "array",
|
|
249
|
+
items: { type: "object" },
|
|
250
|
+
description: "Array of JSON objects representing the rows to insert."
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
required: ["columns", "rows"]
|
|
254
|
+
}
|
|
185
255
|
}
|
|
186
256
|
]
|
|
187
257
|
};
|
|
@@ -194,7 +264,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
194
264
|
content: [{ type: "text", text: "Invalid arguments provided." }]
|
|
195
265
|
};
|
|
196
266
|
}
|
|
197
|
-
if (name !== "find_excel_files") {
|
|
267
|
+
if (name !== "find_excel_files" && name !== "create_new_workbook") {
|
|
198
268
|
if (!args.filePath) {
|
|
199
269
|
return {
|
|
200
270
|
isError: true,
|
|
@@ -202,9 +272,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
202
272
|
};
|
|
203
273
|
}
|
|
204
274
|
}
|
|
275
|
+
else if (name === "create_new_workbook") {
|
|
276
|
+
// Path validation handled within the case statement
|
|
277
|
+
}
|
|
205
278
|
let absolutePath = "";
|
|
206
279
|
try {
|
|
207
|
-
if (name
|
|
280
|
+
if (name === "create_new_workbook") {
|
|
281
|
+
// Path validation handled within the case statement
|
|
282
|
+
if (args.filePath) {
|
|
283
|
+
absolutePath = await validateNewFilePath(args.filePath);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else if (name !== "find_excel_files") {
|
|
208
287
|
absolutePath = await validateFilePath(args.filePath);
|
|
209
288
|
}
|
|
210
289
|
}
|
|
@@ -398,6 +477,96 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
398
477
|
content: [{ type: "text", text: `Successfully updated cell ${cellAddress} to ${newValue}` }]
|
|
399
478
|
};
|
|
400
479
|
}
|
|
480
|
+
case "add_worksheet_with_data": {
|
|
481
|
+
const { sheetName, columns, rows } = args;
|
|
482
|
+
await workbook.xlsx.readFile(absolutePath);
|
|
483
|
+
// Check if sheet exists to avoid crashing
|
|
484
|
+
if (workbook.getWorksheet(sheetName)) {
|
|
485
|
+
throw new Error(`A worksheet named '${sheetName}' already exists in this file.`);
|
|
486
|
+
}
|
|
487
|
+
const ws = workbook.addWorksheet(sheetName);
|
|
488
|
+
// Set columns
|
|
489
|
+
if (Array.isArray(columns)) {
|
|
490
|
+
ws.columns = columns.map(col => ({
|
|
491
|
+
header: col.header,
|
|
492
|
+
key: col.key,
|
|
493
|
+
width: 20 // Default reasonable width
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
// Add data rows
|
|
497
|
+
if (Array.isArray(rows)) {
|
|
498
|
+
rows.forEach(rowData => {
|
|
499
|
+
ws.addRow(rowData);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
// Make the header bold to look professional
|
|
503
|
+
const headerRow = ws.getRow(1);
|
|
504
|
+
headerRow.font = { bold: true };
|
|
505
|
+
headerRow.commit();
|
|
506
|
+
await workbook.xlsx.writeFile(absolutePath);
|
|
507
|
+
return {
|
|
508
|
+
content: [{ type: "text", text: `Successfully generated new reporting sheet '${sheetName}' with ${rows.length} rows of data and saved it to the file.` }]
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
case "create_new_workbook": {
|
|
512
|
+
let finalFilePath = absolutePath;
|
|
513
|
+
const { sheetName = "Sheet1", columns, rows, referenceFilePath, fileName } = args;
|
|
514
|
+
// Handle path resolution
|
|
515
|
+
if (!args.filePath) {
|
|
516
|
+
if (!referenceFilePath || !fileName) {
|
|
517
|
+
return {
|
|
518
|
+
isError: true,
|
|
519
|
+
content: [{ type: "text", text: "Either provide 'filePath' OR both 'referenceFilePath' and 'fileName'." }]
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const refPath = await validateFilePath(args.referenceFilePath);
|
|
524
|
+
const refDir = path.dirname(refPath);
|
|
525
|
+
finalFilePath = path.join(refDir, fileName);
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
return {
|
|
529
|
+
isError: true,
|
|
530
|
+
content: [{ type: "text", text: error.message }]
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
const stats = await fs.stat(finalFilePath);
|
|
536
|
+
if (stats) {
|
|
537
|
+
throw new Error(`File already exists at '${finalFilePath}'. Use add_worksheet_with_data or update_cell to modify it, or use a different file path.`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
catch (e) {
|
|
541
|
+
if (e.code !== 'ENOENT') {
|
|
542
|
+
throw e;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const newWorkbook = new ExcelJS.Workbook();
|
|
546
|
+
const ws = newWorkbook.addWorksheet(sheetName);
|
|
547
|
+
// Set columns
|
|
548
|
+
if (Array.isArray(columns)) {
|
|
549
|
+
ws.columns = columns.map(col => ({
|
|
550
|
+
header: col.header,
|
|
551
|
+
key: col.key,
|
|
552
|
+
width: 20
|
|
553
|
+
}));
|
|
554
|
+
}
|
|
555
|
+
// Add data rows
|
|
556
|
+
if (Array.isArray(rows)) {
|
|
557
|
+
rows.forEach(rowData => {
|
|
558
|
+
ws.addRow(rowData);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
// Make the header bold
|
|
562
|
+
const headerRow = ws.getRow(1);
|
|
563
|
+
headerRow.font = { bold: true };
|
|
564
|
+
headerRow.commit();
|
|
565
|
+
await newWorkbook.xlsx.writeFile(finalFilePath);
|
|
566
|
+
return {
|
|
567
|
+
content: [{ type: "text", text: `Successfully created new workbook '${finalFilePath}' with sheet '${sheetName}' containing ${rows?.length || 0} rows of data.` }]
|
|
568
|
+
};
|
|
569
|
+
}
|
|
401
570
|
default:
|
|
402
571
|
throw new Error(`Unknown tool: ${name}`);
|
|
403
572
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@makeshkumar/mcp-xl-reader",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -21,11 +21,12 @@
|
|
|
21
21
|
"xlsx",
|
|
22
22
|
"ai",
|
|
23
23
|
"claude",
|
|
24
|
-
"tools"
|
|
24
|
+
"tools",
|
|
25
|
+
"workbook"
|
|
25
26
|
],
|
|
26
27
|
"author": "Makesh Kumar",
|
|
27
28
|
"license": "MIT",
|
|
28
|
-
"description": "An MCP server to read, search, and update local Excel (.xlsx) files.",
|
|
29
|
+
"description": "An MCP server to read, search, and update local Excel (.xlsx) files. Create new workbooks with reference path support.",
|
|
29
30
|
"dependencies": {
|
|
30
31
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
31
32
|
"exceljs": "^4.4.0"
|