pythonanywhere-mcp-server 0.0.5__tar.gz → 0.0.7__tar.gz

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.
Files changed (27) hide show
  1. {pythonanywhere_mcp_server-0.0.5/src/pythonanywhere_mcp_server.egg-info → pythonanywhere_mcp_server-0.0.7}/PKG-INFO +2 -2
  2. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/pyproject.toml +2 -2
  3. pythonanywhere_mcp_server-0.0.7/src/pythonanywhere_mcp_server/__init__.py +1 -0
  4. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/src/pythonanywhere_mcp_server/server.py +4 -0
  5. pythonanywhere_mcp_server-0.0.7/src/pythonanywhere_mcp_server/tools/webapp.py +240 -0
  6. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/src/pythonanywhere_mcp_server/tools/website.py +4 -1
  7. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7/src/pythonanywhere_mcp_server.egg-info}/PKG-INFO +2 -2
  8. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/src/pythonanywhere_mcp_server.egg-info/requires.txt +1 -1
  9. pythonanywhere_mcp_server-0.0.7/tests/test_webapp_tools.py +131 -0
  10. pythonanywhere_mcp_server-0.0.5/src/pythonanywhere_mcp_server/__init__.py +0 -0
  11. pythonanywhere_mcp_server-0.0.5/src/pythonanywhere_mcp_server/tools/webapp.py +0 -30
  12. pythonanywhere_mcp_server-0.0.5/tests/test_webapp_tools.py +0 -30
  13. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/LICENSE +0 -0
  14. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/README.md +0 -0
  15. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/setup.cfg +0 -0
  16. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/src/pythonanywhere_mcp_server/__main__.py +0 -0
  17. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/src/pythonanywhere_mcp_server/tools/__init__.py +0 -0
  18. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/src/pythonanywhere_mcp_server/tools/file.py +0 -0
  19. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/src/pythonanywhere_mcp_server/tools/schedule.py +0 -0
  20. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/src/pythonanywhere_mcp_server.egg-info/SOURCES.txt +0 -0
  21. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/src/pythonanywhere_mcp_server.egg-info/dependency_links.txt +0 -0
  22. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/src/pythonanywhere_mcp_server.egg-info/entry_points.txt +0 -0
  23. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/src/pythonanywhere_mcp_server.egg-info/top_level.txt +0 -0
  24. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/tests/test_file_tools.py +0 -0
  25. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/tests/test_schedule_tools.py +0 -0
  26. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/tests/test_server.py +0 -0
  27. {pythonanywhere_mcp_server-0.0.5 → pythonanywhere_mcp_server-0.0.7}/tests/test_website_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonanywhere-mcp-server
3
- Version: 0.0.5
3
+ Version: 0.0.7
4
4
  Summary: PythonAnywhere Model Context Protocol Server
5
5
  Author-email: PythonAnywhere Developers <developers@pythonanywhere.com>
6
6
  License-Expression: MIT
@@ -17,7 +17,7 @@ Classifier: Topic :: Utilities
17
17
  Requires-Python: >=3.13
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: pythonanywhere-core
20
+ Requires-Dist: pythonanywhere-core>=0.2.9
21
21
  Requires-Dist: mcp[cli]
22
22
  Provides-Extra: test
23
23
  Requires-Dist: pytest; extra == "test"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythonanywhere-mcp-server"
7
- version = "0.0.5"
7
+ version = "0.0.7"
8
8
  description = "PythonAnywhere Model Context Protocol Server"
9
9
  authors = [
10
10
  {name = "PythonAnywhere Developers", email = "developers@pythonanywhere.com"}
@@ -12,7 +12,7 @@ authors = [
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.13"
14
14
  dependencies = [
15
- "pythonanywhere-core",
15
+ "pythonanywhere-core>=0.2.9",
16
16
  "mcp[cli]",
17
17
  ]
18
18
  classifiers = [
@@ -0,0 +1 @@
1
+ __version__ = "0.0.7"
@@ -1,6 +1,7 @@
1
1
  import os
2
2
 
3
3
  from mcp.server.fastmcp import FastMCP
4
+ from . import __version__
4
5
  from .tools.file import register_file_tools
5
6
  from .tools.webapp import register_webapp_tools
6
7
  from .tools.website import register_website_tools
@@ -9,6 +10,9 @@ from .tools.schedule import register_schedule_tools
9
10
 
10
11
  def create_server():
11
12
  """Create the MCP server."""
13
+ # Set client identifier for API analytics
14
+ os.environ["PYTHONANYWHERE_CLIENT"] = f"mcp-server/{__version__}"
15
+
12
16
  API_TOKEN = os.getenv("API_TOKEN")
13
17
  if not API_TOKEN:
14
18
  raise RuntimeError("API_TOKEN environment variable must be set.")
@@ -0,0 +1,240 @@
1
+ from pathlib import Path
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+
5
+ from pythonanywhere_core.webapp import Webapp
6
+ from pythonanywhere_core.base import AuthenticationError, NoTokenError
7
+ from pythonanywhere_core.exceptions import MissingCNAMEException
8
+
9
+ # ToDo: Add the log file functions once pythonanywhere-core webapp log file functions
10
+ # have been improved
11
+
12
+ def register_webapp_tools(mcp: FastMCP) -> None:
13
+ @mcp.tool()
14
+ def reload_webapp(domain: str) -> str:
15
+ """
16
+ Reload a uWSGI-based web application for the given domain.
17
+
18
+ This reloads uwsgi-based web applications on PythonAnywhere. For ASGI-based
19
+ web applications, use the `reload_website` tool instead. Any changes to the
20
+ code require a reload to take effect.
21
+
22
+ Args:
23
+ domain (str): The domain name of the web application to reload
24
+ (e.g., 'alice.pythonanywhere.com').
25
+
26
+ Returns:
27
+ str: Status message indicating reload's result.
28
+ """
29
+ try:
30
+ Webapp(domain).reload()
31
+ return f"Webapp '{domain}' reloaded."
32
+ except MissingCNAMEException as exc:
33
+ return f"Webapp '{domain}' reloaded. Note: {str(exc)}"
34
+ except (AuthenticationError, NoTokenError):
35
+ raise RuntimeError("Authentication failed — check API_TOKEN and domain.")
36
+ except Exception as exc:
37
+ raise RuntimeError(str(exc)) from exc
38
+
39
+ @mcp.tool()
40
+ def create_webapp(domain: str, python_version: str, virtualenv_path: str, project_path: str) -> str:
41
+ """
42
+ Create a new uWSGI-based web application for the given domain.
43
+
44
+ This creates a new webapp on PythonAnywhere with the specified configuration.
45
+ The webapp will be created using the specified Python version, virtual environment,
46
+ and project path. If a webapp already exists for this domain, it will fail unless
47
+ the nuke parameter is set to True.
48
+
49
+ The WSGI configuration file can be found in /var/www. The file is of the format:
50
+ domain.replace('.', '_') + '_wsgi.py' It would be automatically created as side effect of running this tool.
51
+
52
+ Note:
53
+ Virtual environments should be configured in the PythonAnywhere web app
54
+ configuration, not in the WSGI file itself.
55
+
56
+ Args:
57
+ domain (str): The domain name for the new webapp (e.g., 'alice.pythonanywhere.com').
58
+ python_version (str): Python version to use (e.g., '3.11', '3.10', '3.9').
59
+ virtualenv_path (str | None): Path to the virtual environment to use. (or None for
60
+ no virtualenv when using one of pre-installed Pythons with batteries included packages)
61
+ project_path (str): Path to the project directory containing the webapp code.
62
+
63
+ Returns:
64
+ str: Status message indicating creation result.
65
+
66
+ Raises:
67
+ RuntimeError: If authentication fails, webapp already exists (and nuke=False),
68
+ or other API errors occur. If 403 error is raised, it may mean that there
69
+ is already a non-uwsgi-based website. `list_websites` tool can be used to check that.
70
+ """
71
+ try:
72
+ webapp = Webapp(domain)
73
+ webapp.create(
74
+ python_version=python_version,
75
+ virtualenv_path=Path(virtualenv_path),
76
+ project_path=Path(project_path),
77
+ nuke=False
78
+ )
79
+ return f"Webapp '{domain}' created successfully."
80
+ except (AuthenticationError, NoTokenError):
81
+ raise RuntimeError("Authentication failed — check API_TOKEN and domain.")
82
+ except Exception as exc:
83
+ raise RuntimeError(str(exc)) from exc
84
+
85
+ @mcp.tool()
86
+ def delete_webapp(domain: str) -> str:
87
+ """
88
+ Delete a uWSGI-based web application for the given domain.
89
+
90
+ This permanently deletes the webapp configuration from PythonAnywhere. The actual
91
+ files in your file system are not deleted, only the webapp configuration that
92
+ serves them. This action cannot be undone.
93
+
94
+ Args:
95
+ domain (str): The domain name of the webapp to delete
96
+ (e.g., 'alice.pythonanywhere.com').
97
+
98
+ Returns:
99
+ str: Status message indicating deletion result.
100
+
101
+ Raises:
102
+ RuntimeError: If authentication fails, webapp doesn't exist, or other API errors occur.
103
+ """
104
+ try:
105
+ Webapp(domain).delete()
106
+ return f"Webapp '{domain}' deleted successfully."
107
+ except (AuthenticationError, NoTokenError):
108
+ raise RuntimeError("Authentication failed — check API_TOKEN and domain.")
109
+ except Exception as exc:
110
+ raise RuntimeError(str(exc)) from exc
111
+
112
+ @mcp.tool()
113
+ def patch_webapp(domain: str, data: dict) -> dict:
114
+ """
115
+ Update configuration settings for a uWSGI-based web application.
116
+
117
+ This allows you to modify various webapp settings such as the Python version,
118
+ virtual environment path, source directory, and other configuration options.
119
+ Only the fields provided in the data dictionary will be updated.
120
+
121
+ In order for any changes to take effect you must reload the webapp.
122
+
123
+ If you provide an invalid Python version you will receive a 400 error with an error
124
+ message that the version you used is not a valid choice. You will need to choose a
125
+ different Python version if you get this error.
126
+
127
+ Args:
128
+ domain (str): The domain name of the webapp to update
129
+ (e.g., 'alice.pythonanywhere.com').
130
+ data (dict): Dictionary containing the configuration updates. Supported keys are:
131
+ - 'python_version': Python version (e.g., '3.11')
132
+ - 'virtualenv_path': Path to virtual environment
133
+ - 'source_directory': Path to source code directory
134
+ - 'working_directory': Working directory for the webapp
135
+ - 'force_https': Force the use of HTTPS when accessing the webapp
136
+ - 'password_protection_enabled': Enable basic HTTP password to your webapp, provided via PythonAnywhere not in the webapp code
137
+ - 'password_protection_username': The username used for HTTP password protection
138
+ - 'password_protection_password': The password used for HTTP password protection
139
+
140
+ Returns:
141
+ dict: Updated webapp configuration information.
142
+ Example: {
143
+ "id": 2097234,
144
+ "user": username,
145
+ "domain_name": domain,
146
+ "python_version": "3.10",
147
+ "source_directory": f"/home/{username}/mysite",
148
+ "working_directory": f"/home/{username}/",
149
+ "virtualenv_path": "",
150
+ "expiry": "2025-10-16",
151
+ "force_https": False,
152
+ "password_protection_enabled": False,
153
+ "password_protection_username": "foo",
154
+ "password_protection_password": "bar"
155
+ }
156
+
157
+ Raises:
158
+ RuntimeError: If authentication fails, webapp doesn't exist, or other API errors occur.
159
+ """
160
+ try:
161
+ result = Webapp(domain).patch(data)
162
+ return result
163
+ except (AuthenticationError, NoTokenError):
164
+ raise RuntimeError("Authentication failed — check API_TOKEN and domain.")
165
+ except Exception as exc:
166
+ raise RuntimeError(str(exc)) from exc
167
+
168
+ @mcp.tool()
169
+ def list_webapps() -> list:
170
+ """
171
+ List all uWSGI-based web applications for the current user.
172
+
173
+ This retrieves information about all webapps configured in your PythonAnywhere
174
+ account. The returned list contains dictionaries with detailed information about
175
+ each webapp including domain, Python version, paths, and status.
176
+
177
+ On PythonAnywhere one may also have non-uWSGI-based websites (usually ASGI-based),
178
+ which are not included in this list. For those, use the `list_websites` tool.
179
+
180
+ Returns:
181
+ list: List of dictionaries containing webapp information. Empty list means
182
+ no WSGI-based webapps are deployed. That still could mean that there are
183
+ non-WSGI-based apps that can be listed with the `list_websites` tool.
184
+
185
+ See also:
186
+ get_webapp_info: Get detailed information about a specific webapp.
187
+
188
+ Raises:
189
+ RuntimeError: If authentication fails or other API errors occur.
190
+ """
191
+ try:
192
+ result = Webapp.list_webapps()
193
+ return result
194
+ except (AuthenticationError, NoTokenError):
195
+ raise RuntimeError("Authentication failed — check API_TOKEN.")
196
+ except Exception as exc:
197
+ raise RuntimeError(str(exc)) from exc
198
+
199
+ @mcp.tool()
200
+ def get_webapp_info(domain: str) -> dict:
201
+ """
202
+ Get detailed information about a specific uWSGI-based web application.
203
+
204
+ This retrieves comprehensive configuration and status information for the
205
+ specified webapp, including paths, Python version, enabled status, and
206
+ other configuration details.
207
+
208
+ Args:
209
+ domain (str): The domain name of the webapp to get information for
210
+ (e.g., 'alice.pythonanywhere.com').
211
+
212
+ Returns:
213
+ dict: Dictionary containing detailed webapp information including:
214
+ - "id": int, # Unique identifier for the site or user session
215
+ - "user": str, # Username associated with the deployment
216
+ - "domain_name": str, # Domain name for the deployed site
217
+ - "python_version": str, # Python version used, e.g., "3.10"
218
+ - "source_directory": str, # Absolute path to the site's source directory
219
+ - "working_directory": str, # Absolute path to the working directory
220
+ - "virtualenv_path": str, # Path to the Python virtual environment (can be empty)
221
+ - "expiry": str, # Expiration date in ISO format, e.g., "2025-10-16"
222
+ - "force_https": bool, # Whether HTTPS is enforced
223
+ - "password_protection_enabled": bool, # Whether password protection is enabled
224
+ - "password_protection_username": str, # Username for password-protected access
225
+ - "password_protection_password": str # Password for password-protected access
226
+
227
+
228
+ Raises:
229
+ RuntimeError: If authentication fails, webapp doesn't exist, or other API errors occur.
230
+ """
231
+ try:
232
+ result = Webapp(domain).get()
233
+ return result
234
+ except (AuthenticationError, NoTokenError):
235
+ raise RuntimeError("Authentication failed — check API_TOKEN and domain.")
236
+ except Exception as exc:
237
+ raise RuntimeError(str(exc)) from exc
238
+
239
+
240
+
@@ -37,11 +37,14 @@ def register_website_tools(mcp: FastMCP) -> None:
37
37
  Return info dictionaries for every ASGI website configured for the current
38
38
  user. Empty list means that there are no websites deployed.
39
39
  That would not include WSGI-based web applications,
40
- which could be only reloaded with `reload_webapp` tool.
40
+ which could be only listed with the `list_webapps` tool.
41
41
 
42
42
 
43
43
  Returns:
44
44
  List[dict[str, Any]]: List of dictionaries with website information.
45
+ Empty list if no websites are configured for the user, but there could be
46
+ still WSGI-based web applications configured that qould be listed with
47
+ the `list_webapps` tool.
45
48
 
46
49
  """
47
50
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonanywhere-mcp-server
3
- Version: 0.0.5
3
+ Version: 0.0.7
4
4
  Summary: PythonAnywhere Model Context Protocol Server
5
5
  Author-email: PythonAnywhere Developers <developers@pythonanywhere.com>
6
6
  License-Expression: MIT
@@ -17,7 +17,7 @@ Classifier: Topic :: Utilities
17
17
  Requires-Python: >=3.13
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: pythonanywhere-core
20
+ Requires-Dist: pythonanywhere-core>=0.2.9
21
21
  Requires-Dist: mcp[cli]
22
22
  Provides-Extra: test
23
23
  Requires-Dist: pytest; extra == "test"
@@ -1,4 +1,4 @@
1
- pythonanywhere-core
1
+ pythonanywhere-core>=0.2.9
2
2
  mcp[cli]
3
3
 
4
4
  [test]
@@ -0,0 +1,131 @@
1
+ import pytest
2
+ from pathlib import Path
3
+ from pythonanywhere_core.base import AuthenticationError
4
+ from pythonanywhere_core.exceptions import MissingCNAMEException
5
+
6
+ import tools.webapp as webapp_tools
7
+
8
+
9
+ @pytest.fixture
10
+ def setup_webapp_tools(mcp):
11
+ webapp_tools.register_webapp_tools(mcp)
12
+ return mcp
13
+
14
+
15
+ @pytest.mark.parametrize("tool_name,method_name,params,expected_params,expected_result", [
16
+ ("reload_webapp", "reload", {"domain": "test.com"}, {}, "Webapp 'test.com' reloaded."),
17
+ ("delete_webapp", "delete", {"domain": "test.com"}, {}, "Webapp 'test.com' deleted successfully."),
18
+ ("get_webapp_info", "get", {"domain": "test.com"}, {}, {"domain_name": "test.com", "python_version": "3.10"}),
19
+ ("patch_webapp", "patch", {"domain": "test.com", "data": {"python_version": "3.10"}}, {"python_version": "3.10"}, {"domain_name": "test.com", "python_version": "3.10"}),
20
+ ])
21
+ def test_webapp_tools_success(setup_webapp_tools, mocker, tool_name, method_name, params, expected_params, expected_result):
22
+ mock_webapp = mocker.patch("tools.webapp.Webapp", autospec=True)
23
+
24
+ if tool_name == "get_webapp_info" or tool_name == "patch_webapp":
25
+ getattr(mock_webapp.return_value, method_name).return_value = expected_result
26
+
27
+ result = setup_webapp_tools.call_tool(tool_name, params)
28
+
29
+ if "domain" in params:
30
+ mock_webapp.assert_called_with(params["domain"])
31
+ if expected_params:
32
+ getattr(mock_webapp.return_value, method_name).assert_called_with(expected_params)
33
+ else:
34
+ getattr(mock_webapp.return_value, method_name).assert_called_once()
35
+
36
+ assert result == expected_result
37
+
38
+
39
+ @pytest.mark.parametrize("tool_name,method_name,params,side_effect,expected_error", [
40
+ ("reload_webapp", "reload", {"domain": "test.com"}, AuthenticationError(), "Authentication failed"),
41
+ ("reload_webapp", "reload", {"domain": "test.com"}, Exception("webapp reload error"), "webapp reload error"),
42
+ ("delete_webapp", "delete", {"domain": "test.com"}, AuthenticationError(), "Authentication failed"),
43
+ ("delete_webapp", "delete", {"domain": "test.com"}, Exception("delete error"), "delete error"),
44
+ ("get_webapp_info", "get", {"domain": "test.com"}, AuthenticationError(), "Authentication failed"),
45
+ ("get_webapp_info", "get", {"domain": "test.com"}, Exception("info error"), "info error"),
46
+ ("patch_webapp", "patch", {"domain": "test.com", "data": {"python_version": "3.10"}}, AuthenticationError(), "Authentication failed"),
47
+ ("patch_webapp", "patch", {"domain": "test.com", "data": {"python_version": "3.10"}}, Exception("patch error"), "patch error"),
48
+ ])
49
+ def test_webapp_tools_errors(setup_webapp_tools, mocker, tool_name, method_name, params, side_effect, expected_error):
50
+ mock_webapp = mocker.patch("tools.webapp.Webapp", autospec=True)
51
+ getattr(mock_webapp.return_value, method_name).side_effect = side_effect
52
+
53
+ with pytest.raises(RuntimeError) as exc:
54
+ setup_webapp_tools.call_tool(tool_name, params)
55
+ assert expected_error in str(exc)
56
+
57
+
58
+ @pytest.mark.parametrize("side_effect,expected_error", [
59
+ (AuthenticationError(), "Authentication failed"),
60
+ (Exception("list error"), "list error"),
61
+ ])
62
+ def test_list_webapps_errors(setup_webapp_tools, mocker, side_effect, expected_error):
63
+ mocker.patch("tools.webapp.Webapp", autospec=True)
64
+ mocker.patch("tools.webapp.Webapp.list_webapps", side_effect=side_effect)
65
+
66
+ with pytest.raises(RuntimeError) as exc:
67
+ setup_webapp_tools.call_tool("list_webapps", {})
68
+ assert expected_error in str(exc)
69
+
70
+
71
+ def test_create_webapp(setup_webapp_tools, mocker):
72
+ webapp_domain = 'test.com'
73
+ python_version = '3.10'
74
+ virtualenv_path = Path('/test/venv/path')
75
+ project_path = Path('/project/path')
76
+
77
+ mock_webapp = mocker.patch("tools.webapp.Webapp", autospec=True)
78
+ result = setup_webapp_tools.call_tool(
79
+ "create_webapp",
80
+ {
81
+ "domain": webapp_domain,
82
+ "python_version": python_version,
83
+ "virtualenv_path": virtualenv_path,
84
+ "project_path": project_path,
85
+ }
86
+ )
87
+ mock_webapp.assert_called_with(webapp_domain)
88
+ mock_webapp.return_value.create.assert_called_with(
89
+ python_version=python_version,
90
+ virtualenv_path=virtualenv_path,
91
+ project_path=project_path,
92
+ nuke=False
93
+ )
94
+ mock_webapp.return_value.create.assert_called_once()
95
+ assert result == f"Webapp 'test.com' created successfully."
96
+
97
+
98
+ @pytest.mark.parametrize("side_effect,expected_error", [
99
+ (AuthenticationError(), "Authentication failed"),
100
+ (Exception("webapp create error"), "webapp create error"),
101
+ ])
102
+ def test_create_webapp_errors(setup_webapp_tools, mocker, side_effect, expected_error):
103
+ mock_webapp = mocker.patch("tools.webapp.Webapp", autospec=True)
104
+ mock_webapp.return_value.create.side_effect = side_effect
105
+ params = {
106
+ "domain": "test.com",
107
+ "python_version": "3.10",
108
+ "virtualenv_path": "/test/venv/path",
109
+ "project_path": "/project/path",
110
+ }
111
+ with pytest.raises(RuntimeError) as exc:
112
+ setup_webapp_tools.call_tool("create_webapp", params)
113
+ assert expected_error in str(exc)
114
+
115
+
116
+ def test_list_webapps(setup_webapp_tools, mocker):
117
+ mocker.patch("tools.webapp.Webapp", autospec=True)
118
+ expected = [{"domain_name": "test.com", "python_version": "3.10"}]
119
+ mocker.patch("tools.webapp.Webapp.list_webapps", return_value=expected)
120
+ result = setup_webapp_tools.call_tool("list_webapps", {})
121
+ assert result == expected
122
+
123
+
124
+ def test_reload_webapp_handles_missing_cname_exception(setup_webapp_tools, mocker):
125
+ mock_webapp = mocker.patch("tools.webapp.Webapp", autospec=True)
126
+ mock_webapp.return_value.reload.side_effect = MissingCNAMEException()
127
+
128
+ result = setup_webapp_tools.call_tool("reload_webapp", {"domain": "test.com"})
129
+
130
+ assert "Webapp 'test.com' reloaded" in result
131
+ assert "CNAME" in result or "cname" in result
@@ -1,30 +0,0 @@
1
- from mcp.server.fastmcp import FastMCP
2
-
3
- from pythonanywhere_core.webapp import Webapp
4
- from pythonanywhere_core.base import AuthenticationError, NoTokenError
5
-
6
-
7
- def register_webapp_tools(mcp: FastMCP) -> None:
8
- @mcp.tool()
9
- def reload_webapp(domain: str) -> str:
10
- """
11
- Reload a uWSGI-based web application for the given domain.
12
-
13
- This reloads uwsgi-based web applications on PythonAnywhere. For ASGI-based
14
- web applications, use the `reload_website` tool instead. Any changes to the
15
- code require a reload to take effect.
16
-
17
- Args:
18
- domain (str): The domain name of the web application to reload
19
- (e.g., 'alice.pythonanywhere.com').
20
-
21
- Returns:
22
- str: Status message indicating reload's result.
23
- """
24
- try:
25
- Webapp(domain).reload()
26
- return f"Webapp '{domain}' reloaded."
27
- except (AuthenticationError, NoTokenError):
28
- raise RuntimeError("Authentication failed — check API_TOKEN and domain.")
29
- except Exception as exc:
30
- raise RuntimeError(str(exc)) from exc
@@ -1,30 +0,0 @@
1
- import pytest
2
-
3
- import tools.webapp as webapp_tools
4
-
5
- def test_reload_webapp(mcp, mocker):
6
- webapp_tools.register_webapp_tools(mcp)
7
- mock_webapp = mocker.patch("tools.webapp.Webapp", autospec=True)
8
- result = mcp.call_tool("reload_webapp", {"domain": "test.com"})
9
- mock_webapp.assert_called_with("test.com")
10
- mock_webapp.return_value.reload.assert_called_once()
11
- assert result == "Webapp 'test.com' reloaded."
12
-
13
-
14
- def test_reload_webapp_auth_error(mcp, mocker):
15
- webapp_tools.register_webapp_tools(mcp)
16
- mock_webapp = mocker.patch("tools.webapp.Webapp", autospec=True)
17
- from pythonanywhere_core.base import AuthenticationError
18
- mock_webapp.return_value.reload.side_effect = AuthenticationError()
19
- with pytest.raises(RuntimeError) as exc:
20
- mcp.call_tool("reload_webapp", {"domain": "test.com"})
21
- assert "Authentication failed" in str(exc)
22
-
23
-
24
- def test_reload_webapp_other_error(mcp, mocker):
25
- webapp_tools.register_webapp_tools(mcp)
26
- mock_webapp = mocker.patch("tools.webapp.Webapp", autospec=True)
27
- mock_webapp.return_value.reload.side_effect = Exception("webapp reload error")
28
- with pytest.raises(RuntimeError) as exc:
29
- mcp.call_tool("reload_webapp", {"domain": "test.com"})
30
- assert "webapp reload error" in str(exc)