Skip to content

Commit e9d26c9

Browse files
authored
Merge pull request #2 from jonemo/default-folders
Look for file in Desktop and Downloads directories if not given full path.
2 parents 435d7a5 + adcaf3c commit e9d26c9

File tree

3 files changed

+69
-16
lines changed

3 files changed

+69
-16
lines changed

.github/workflows/tests.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ on:
88

99
jobs:
1010
test:
11-
runs-on: ubuntu-latest
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
matrix:
14+
os: [windows-latest, macos-latest]
1215

1316
steps:
1417
- uses: actions/checkout@v4

openpyxl_mcp_server.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
"e": "error",
2222
}
2323

24+
FILEPATH_DOCSTRING = 'The path to the Excel workbook. For example, "~/Downloads/test.xlsx" or "C:\\myfolder\\myfile.xlsx". If only a filename is provided, the file will be searched for in the Desktop and Downloads folders.'
25+
2426

2527
@mcp.tool()
2628
async def get_cell_details(filepath: str, sheet_name: str, cell_name: str) -> str:
27-
"""Get value, data type, style, comments, formulas, hyperlinks, and other details for a single cell in a workbook.
29+
f"""Get value, data type, style, comments, formulas, hyperlinks, and other details for a single cell in a workbook.
2830
2931
Args:
30-
file_path: The path to the Excel workbook. For example, "~/Downloads/test.xlsx" or "C:\\myfolder\\myfile.xlsx".
32+
file_path: {FILEPATH_DOCSTRING}
3133
sheet_name: The name of the sheet to get the value from.
3234
cell name: The name of the cell to get the value from. For example, "A1", "B2", "R5987".
3335
"""
@@ -199,10 +201,10 @@ async def get_cell_details(filepath: str, sheet_name: str, cell_name: str) -> st
199201

200202
@mcp.tool()
201203
async def get_cell_value(filepath: str, sheet_name: str, cell_name: str) -> str:
202-
"""Get the raw value of a single cell in a workbook.
204+
f"""Get the raw value of a single cell in a workbook.
203205
204206
Args:
205-
file_path: The path to the Excel workbook. For example, "~/Downloads/test.xlsx" or "C:\\myfolder\\myfile.xlsx".
207+
file_path: {FILEPATH_DOCSTRING}
206208
sheet_name: The name of the sheet to get the value from.
207209
cell name: The name of the cell to get the value from. For example, "A1", "B2", "R5987".
208210
"""
@@ -217,10 +219,10 @@ async def get_cell_value(filepath: str, sheet_name: str, cell_name: str) -> str:
217219
async def get_values_of_cell_range(
218220
filepath: str, sheet_name: str, top_left_cell: str, bottom_right_cell: str
219221
) -> str:
220-
"""Get the value, data type, style, and any comments, of a continuous range of cells in an Excel workbook.
222+
f"""Get the value, data type, style, and any comments, of a continuous range of cells in an Excel workbook.
221223
222224
Args:
223-
file_path: The path to the Excel workbook. For example, "~/Downloads/test.xlsx" or "C:\\myfolder\\myfile.xlsx".
225+
file_path: {FILEPATH_DOCSTRING}
224226
sheet_name: The name of the sheet to get the value from.
225227
top_left_cell: The top left cell of the range. For example, "A1".
226228
bottom_right_cell: The bottom right cell of the range. For example, "RC976".
@@ -242,10 +244,10 @@ async def get_values_of_cell_range(
242244
async def get_content_of_cell_list(
243245
filepath: str, sheet_name: str, cell_name_list: list[str]
244246
) -> str:
245-
"""Get the raw values of a list of specific named cells in an Excel workbook.
247+
f"""Get the raw values of a list of specific named cells in an Excel workbook.
246248
247249
Args:
248-
file_path: The path to the Excel workbook. For example, "~/Downloads/test.xlsx" or "C:\\myfolder\\myfile.xlsx".
250+
file_path: {FILEPATH_DOCSTRING}
249251
sheet_name: The name of the sheet to get the value from.
250252
cell_name_list: A list of cell names. For example, ["A1", "B2", "C3"].
251253
"""
@@ -268,10 +270,10 @@ async def search_in_cell_range(
268270
search_string: str,
269271
exact_match: bool = False,
270272
) -> str:
271-
"""Search for a string in a continuous range of cells in an Excel workbook.
273+
f"""Search for a string in a continuous range of cells in an Excel workbook.
272274
273275
Args:
274-
file_path: The path to the Excel workbook. For example, "~/Downloads/test.xlsx" or "C:\\myfolder\\myfile.xlsx".
276+
file_path: {FILEPATH_DOCSTRING}
275277
sheet_name: The name of the sheet to get the value from.
276278
top_left_cell: The top left cell of the range. For example, "A1".
277279
bottom_right_cell: The bottom right cell of the range. For example, "RC976".
@@ -298,10 +300,10 @@ async def search_in_cell_range(
298300

299301
@mcp.tool()
300302
async def get_list_of_sheets(filepath: str) -> str:
301-
"""Get a list of sheets in an Excel workbook. Each line contains a sheet's name and dimensions.
303+
f"""Get a list of sheets in an Excel workbook. Each line contains a sheet's name and dimensions.
302304
303305
Args:
304-
file_path: The path to the Excel workbook. For example, "~/Downloads/test.xlsx" or "C:\\myfolder\\myfile.xlsx".
306+
file_path: {FILEPATH_DOCSTRING}
305307
"""
306308
filepath_clean = resolve_path_and_assert_file_exists(filepath)
307309
wb = load_workbook(filename=filepath_clean)
@@ -313,9 +315,20 @@ async def get_list_of_sheets(filepath: str) -> str:
313315

314316
def resolve_path_and_assert_file_exists(filepath: str) -> Path:
315317
expanded_path = Path(filepath).expanduser()
316-
if not expanded_path.exists():
317-
raise ValueError(f"File {expanded_path} does not exist 2")
318-
return expanded_path
318+
if expanded_path.exists():
319+
return expanded_path
320+
# If filepath is just a filename and this is Winodws or MacOS, try the default locations of the Desktop and
321+
# Downloads directories
322+
is_windows = sys.platform == "win32" and "\\" not in filepath
323+
is_macos = sys.platform == "darwin" and "/" not in filepath
324+
if is_windows or is_macos:
325+
path_in_desktop = Path.home() / "Desktop" / filepath
326+
if path_in_desktop.exists():
327+
return path_in_desktop
328+
path_in_downloads = Path.home() / "Downloads" / filepath
329+
if path_in_downloads.exists():
330+
return path_in_downloads
331+
raise ValueError(f"File '{filepath}' does not exist")
319332

320333

321334
def get_sheet_and_assert_it_exists(wb: Workbook, sheet_name: str) -> Worksheet:

test_openpyxl_mcp_server.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import tempfile
2+
from unittest.mock import patch
3+
14
import pytest
25
from pathlib import Path
36
from openpyxl_mcp_server import get_list_of_sheets
7+
from openpyxl_mcp_server import resolve_path_and_assert_file_exists
48

59

610
@pytest.mark.asyncio
@@ -11,4 +15,37 @@ async def test_get_list_of_sheets():
1115
sheets = result.split("\n")
1216
assert len(sheets) == 2
1317
assert sheets[0] == "Name: First Worksheet, Dimensions: A1:F4"
18+
# Yes, you can have use emojis in sheet names:
1419
assert sheets[1] == "Name: 🧮, Dimensions: A1:B1"
20+
21+
22+
def test_resolve_path_and_assert_file_exists_full_path():
23+
test_file = Path(__file__).parent / "testdata" / "simple_workbook.xlsx"
24+
result = resolve_path_and_assert_file_exists(str(test_file))
25+
assert result == test_file
26+
27+
28+
def test_resolve_path_and_assert_file_exists_in_home_folder():
29+
test_file_name = "file_in_home_folder.xlsx"
30+
with tempfile.TemporaryDirectory() as temp_dir:
31+
temp_dir_path = Path(temp_dir)
32+
test_file_path = temp_dir_path / test_file_name
33+
test_file_path.touch()
34+
with patch("openpyxl_mcp_server.Path.expanduser", return_value=test_file_path):
35+
result = resolve_path_and_assert_file_exists(f"~/{test_file_name}")
36+
assert result == test_file_path
37+
38+
39+
@pytest.mark.parametrize("home_folder_name", ["Desktop", "Downloads"])
40+
def test_resolve_path_and_assert_file_exists_in_default_folder(home_folder_name):
41+
test_file_name = "file_in_desktop_folder.xlsx"
42+
# Make a temporary directory with the file, then mock Path.home() to return that directory
43+
with tempfile.TemporaryDirectory() as temp_dir:
44+
temp_dir_path = Path(temp_dir)
45+
home_folder_path = temp_dir_path / home_folder_name
46+
home_folder_path.mkdir(parents=True, exist_ok=True)
47+
test_file_path = home_folder_path / test_file_name
48+
test_file_path.touch()
49+
with patch("openpyxl_mcp_server.Path.home", return_value=temp_dir_path):
50+
result = resolve_path_and_assert_file_exists(test_file_name)
51+
assert result == test_file_path

0 commit comments

Comments
 (0)