Skip to content

Commit f883596

Browse files
adds directory upload.
1 parent 32eced0 commit f883596

File tree

6 files changed

+59
-10
lines changed

6 files changed

+59
-10
lines changed

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ Have your PythonAnywhere API token and username ready. You can find (or
3434
generate) your API token in the [API section of your PythonAnywhere
3535
account](https://www.pythonanywhere.com/account/#api_token).
3636

37+
If your account is on `eu.pythonanywhere.com`, you also need to set
38+
`PYTHONANYWHERE_SITE` to `eu.pythonanywhere.com` (it defaults to
39+
`www.pythonanywhere.com`).
40+
3741
### Desktop Extension - works with Claude Desktop
3842
Probably the most straightforward way to install the MCP server is to use
3943
the [desktop extension](https://github.com/anthropics/dxt/) for Claude Desktop.
@@ -49,7 +53,7 @@ Run:
4953
```bash
5054
claude mcp add pythonanywhere-mcp-server \
5155
-e API_TOKEN=yourpythonanywhereapitoken \
52-
-e LOGNAME=yourpythonanywhereusername \
56+
-e PYTHONANYWHERE_USERNAME=yourpythonanywhereusername \
5357
-- uvx pythonanywhere-mcp-server
5458
```
5559

@@ -65,7 +69,7 @@ Add it to your `mcp.json`.
6569
"args": ["pythonanywhere-mcp-server"],
6670
"env": {
6771
"API_TOKEN": "yourpythonanywhereapitoken",
68-
"LOGNAME": "yourpythonanywhereusername"
72+
"PYTHONANYWHERE_USERNAME": "yourpythonanywhereusername"
6973
}
7074
}
7175
}
@@ -85,7 +89,7 @@ for Cursor).
8589
"args": ["pythonanywhere-mcp-server"],
8690
"env": {
8791
"API_TOKEN": "yourpythonanywhereapitoken",
88-
"LOGNAME": "yourpythonanywhereusername"
92+
"PYTHONANYWHERE_USERNAME": "yourpythonanywhereusername"
8993
}
9094
}
9195
}

manifest.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
"title": "PythonAnywhere Username",
2222
"description": "Your PythonAnywhere username",
2323
"required": true
24+
},
25+
"pa_site": {
26+
"type": "string",
27+
"title": "PythonAnywhere Site",
28+
"description": "Set to eu.pythonanywhere.com if your account is on the EU site (defaults to www.pythonanywhere.com)",
29+
"required": false
2430
}
2531
},
2632
"server": {
@@ -31,7 +37,8 @@
3137
"args": ["pythonanywhere-mcp-server"],
3238
"env": {
3339
"API_TOKEN": "${user_config.pa_api_token}",
34-
"LOGNAME": "${user_config.pa_username}"
40+
"PYTHONANYWHERE_USERNAME": "${user_config.pa_username}",
41+
"PYTHONANYWHERE_SITE": "${user_config.pa_site}"
3542
}
3643
}
3744
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ authors = [
1212
readme = "README.md"
1313
requires-python = ">=3.13"
1414
dependencies = [
15-
"pythonanywhere-core>=0.2.9",
15+
"pythonanywhere-core>=0.3.0",
1616
"mcp[cli]",
1717
]
1818
classifiers = [

src/pythonanywhere_mcp_server/tools/file.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,27 @@ def upload_text_file(dest_path: str, content: str) -> str:
4444
except Exception as exc:
4545
raise RuntimeError(f"Failed to upload text file: {str(exc)}") from exc
4646

47+
@mcp.tool()
48+
def upload_directory(local_dir_path: str, remote_dir_path: str) -> str:
49+
"""
50+
Upload a local directory to PythonAnywhere, preserving directory structure.
51+
52+
Recursively walks the local directory and uploads each file.
53+
Empty directories are preserved.
54+
55+
Args:
56+
local_dir_path (str): The absolute path to the local directory to upload.
57+
remote_dir_path (str): The absolute path on PythonAnywhere where the directory will be uploaded.
58+
59+
Returns:
60+
str: Status message indicating upload result.
61+
"""
62+
try:
63+
Files().tree_post(local_dir_path, remote_dir_path)
64+
return f"Uploaded {local_dir_path} to {remote_dir_path}."
65+
except Exception as exc:
66+
raise RuntimeError(f"Failed to upload directory: {str(exc)}") from exc
67+
4768
@mcp.tool()
4869
def delete_path(path: str) -> str:
4970
"""

tests/test_file_tools.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,23 @@ def test_delete_path_exception(mcp, mocker):
6262
assert "Failed to delete path: delete error" in str(exc)
6363

6464

65+
def test_upload_directory(mcp, mocker):
66+
file_tools.register_file_tools(mcp)
67+
mock_files = mocker.patch("tools.file.Files", autospec=True)
68+
result = mcp.call_tool("upload_directory", {"local_dir_path": "/local/myapp", "remote_dir_path": "/home/user/myapp"})
69+
mock_files.return_value.tree_post.assert_called_with("/local/myapp", "/home/user/myapp")
70+
assert result == "Uploaded /local/myapp to /home/user/myapp."
71+
72+
73+
def test_upload_directory_exception(mcp, mocker):
74+
file_tools.register_file_tools(mcp)
75+
mock_files = mocker.patch("tools.file.Files", autospec=True)
76+
mock_files.return_value.tree_post.side_effect = Exception("upload dir error")
77+
with pytest.raises(RuntimeError) as exc:
78+
mcp.call_tool("upload_directory", {"local_dir_path": "/local/myapp", "remote_dir_path": "/home/user/myapp"})
79+
assert "Failed to upload directory: upload dir error" in str(exc)
80+
81+
6582
def test_directory_tree(mcp, mocker):
6683
file_tools.register_file_tools(mcp)
6784
mock_files = mocker.patch("tools.file.Files", autospec=True)

uv.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)