@makeshkumar/mcp-xl-reader 1.0.3 → 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 +112 -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);
|
|
@@ -210,6 +222,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
210
222
|
},
|
|
211
223
|
required: ["filePath", "sheetName", "columns", "rows"]
|
|
212
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
|
+
}
|
|
213
255
|
}
|
|
214
256
|
]
|
|
215
257
|
};
|
|
@@ -222,7 +264,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
222
264
|
content: [{ type: "text", text: "Invalid arguments provided." }]
|
|
223
265
|
};
|
|
224
266
|
}
|
|
225
|
-
if (name !== "find_excel_files") {
|
|
267
|
+
if (name !== "find_excel_files" && name !== "create_new_workbook") {
|
|
226
268
|
if (!args.filePath) {
|
|
227
269
|
return {
|
|
228
270
|
isError: true,
|
|
@@ -230,9 +272,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
230
272
|
};
|
|
231
273
|
}
|
|
232
274
|
}
|
|
275
|
+
else if (name === "create_new_workbook") {
|
|
276
|
+
// Path validation handled within the case statement
|
|
277
|
+
}
|
|
233
278
|
let absolutePath = "";
|
|
234
279
|
try {
|
|
235
|
-
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") {
|
|
236
287
|
absolutePath = await validateFilePath(args.filePath);
|
|
237
288
|
}
|
|
238
289
|
}
|
|
@@ -457,6 +508,65 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
457
508
|
content: [{ type: "text", text: `Successfully generated new reporting sheet '${sheetName}' with ${rows.length} rows of data and saved it to the file.` }]
|
|
458
509
|
};
|
|
459
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
|
+
}
|
|
460
570
|
default:
|
|
461
571
|
throw new Error(`Unknown tool: ${name}`);
|
|
462
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"
|