pythonanywhere-mcp-server 0.0.4__tar.gz → 0.0.6__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.
- {pythonanywhere_mcp_server-0.0.4/src/pythonanywhere_mcp_server.egg-info → pythonanywhere_mcp_server-0.0.6}/PKG-INFO +28 -28
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/README.md +28 -28
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/pyproject.toml +1 -1
- pythonanywhere_mcp_server-0.0.6/src/pythonanywhere_mcp_server/tools/webapp.py +237 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/src/pythonanywhere_mcp_server/tools/website.py +4 -1
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6/src/pythonanywhere_mcp_server.egg-info}/PKG-INFO +28 -28
- pythonanywhere_mcp_server-0.0.6/tests/test_webapp_tools.py +120 -0
- pythonanywhere_mcp_server-0.0.4/src/pythonanywhere_mcp_server/tools/webapp.py +0 -30
- pythonanywhere_mcp_server-0.0.4/tests/test_webapp_tools.py +0 -30
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/LICENSE +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/setup.cfg +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/src/pythonanywhere_mcp_server/__init__.py +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/src/pythonanywhere_mcp_server/__main__.py +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/src/pythonanywhere_mcp_server/server.py +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/src/pythonanywhere_mcp_server/tools/__init__.py +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/src/pythonanywhere_mcp_server/tools/file.py +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/src/pythonanywhere_mcp_server/tools/schedule.py +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/src/pythonanywhere_mcp_server.egg-info/SOURCES.txt +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/src/pythonanywhere_mcp_server.egg-info/dependency_links.txt +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/src/pythonanywhere_mcp_server.egg-info/entry_points.txt +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/src/pythonanywhere_mcp_server.egg-info/requires.txt +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/src/pythonanywhere_mcp_server.egg-info/top_level.txt +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/tests/test_file_tools.py +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/tests/test_schedule_tools.py +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/tests/test_server.py +0 -0
- {pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/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.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: PythonAnywhere Model Context Protocol Server
|
|
5
5
|
Author-email: PythonAnywhere Developers <developers@pythonanywhere.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,42 +26,42 @@ Dynamic: license-file
|
|
|
26
26
|
|
|
27
27
|
# PythonAnywhere Model Context Protocol Server
|
|
28
28
|
|
|
29
|
-
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction)
|
|
30
|
-
server acts as a bridge between AI-powered tools and your
|
|
31
|
-
[PythonAnywhere](https://www.pythonanywhere.com/) account, enabling secure,
|
|
32
|
-
programmatic management of files, websites, webapps, and scheduled tasks. By
|
|
33
|
-
exposing a standardized interface, it allows language models and automation
|
|
34
|
-
clients to perform operations—such as editing files, deploying web apps, or
|
|
35
|
-
scheduling jobs
|
|
29
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction)
|
|
30
|
+
server acts as a bridge between AI-powered tools and your
|
|
31
|
+
[PythonAnywhere](https://www.pythonanywhere.com/) account, enabling secure,
|
|
32
|
+
programmatic management of files, websites, webapps, and scheduled tasks. By
|
|
33
|
+
exposing a standardized interface, it allows language models and automation
|
|
34
|
+
clients to perform operations—such as editing files, deploying web apps, or
|
|
35
|
+
scheduling jobs -- on your behalf, all while maintaining fine-grained control
|
|
36
36
|
and auditability.
|
|
37
37
|
|
|
38
38
|
## Features
|
|
39
|
-
- **File management**: Read, upload, delete files and list directory trees.
|
|
39
|
+
- **File management**: Read, upload, delete files and list directory trees.
|
|
40
40
|
_(also enables debugging with direct access to log files, which are just
|
|
41
41
|
files on PythonAnywhere)_
|
|
42
42
|
- **ASGI Web app management**: Create, delete, reload, and list.
|
|
43
|
-
_(as described in the [PythonAnywhere ASGI
|
|
43
|
+
_(as described in the [PythonAnywhere ASGI
|
|
44
44
|
documentation](https://help.pythonanywhere.com/pages/ASGICommandLine))_
|
|
45
45
|
- **WSGI Web app management**: Reload only _(at the moment)_.
|
|
46
46
|
- **Scheduled task management**: List, create, update, and delete.
|
|
47
|
-
_(
|
|
48
|
-
scheduled
|
|
47
|
+
_(Note that this enables LLMs to execute arbitrary commands if a task is
|
|
48
|
+
scheduled too soon after creation and deleted after execution. For that we
|
|
49
49
|
would suggest running it with [mcp-server-time](https://pypi.org/project/mcp-server-time/)
|
|
50
50
|
as models easily get confused about time.)_
|
|
51
51
|
|
|
52
52
|
## Installation
|
|
53
|
-
MCP protocol is well-defined and supported by various clients, but
|
|
54
|
-
installation is different depending on the client you are using. We will
|
|
53
|
+
The MCP protocol is well-defined and supported by various clients, but
|
|
54
|
+
installation is different depending on the client you are using. We will
|
|
55
55
|
cover cases that we tried and tested.
|
|
56
56
|
|
|
57
57
|
In all cases, you need to have `uv` installed and available in your `PATH`.
|
|
58
58
|
|
|
59
|
-
Have your PythonAnywhere API token and username ready. You can find (or
|
|
60
|
-
generate) your API token in the [API section of your PythonAnywhere
|
|
59
|
+
Have your PythonAnywhere API token and username ready. You can find (or
|
|
60
|
+
generate) your API token in the [API section of your PythonAnywhere
|
|
61
61
|
account](https://www.pythonanywhere.com/account/#api_token).
|
|
62
62
|
|
|
63
63
|
### Desktop Extension - works with Claude Desktop
|
|
64
|
-
Probably the most straightforward way to install the MCP server is to use
|
|
64
|
+
Probably the most straightforward way to install the MCP server is to use
|
|
65
65
|
the [desktop extension](https://github.com/anthropics/dxt/) for Claude Desktop.
|
|
66
66
|
|
|
67
67
|
1. Open Claude Desktop.
|
|
@@ -99,7 +99,7 @@ Add it to your `mcp.json`.
|
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
### Claude Desktop (manual setup) and Cursor:
|
|
102
|
-
Add it to `claude_desktop_config.json` (for Claude Desktop) or (`mcp.json`
|
|
102
|
+
Add it to `claude_desktop_config.json` (for Claude Desktop) or (`mcp.json`
|
|
103
103
|
for Cursor).
|
|
104
104
|
|
|
105
105
|
```json
|
|
@@ -120,20 +120,20 @@ for Cursor).
|
|
|
120
120
|
|
|
121
121
|
## Caveats
|
|
122
122
|
|
|
123
|
-
Direct integration of an LLM with your PythonAnywhere account offers
|
|
124
|
-
significant capabilities, but also introduces risks. We strongly advise
|
|
125
|
-
maintaining human oversight, especially for sensitive actions such as
|
|
123
|
+
Direct integration of an LLM with your PythonAnywhere account offers
|
|
124
|
+
significant capabilities, but also introduces risks. We strongly advise
|
|
125
|
+
maintaining human oversight, especially for sensitive actions such as
|
|
126
126
|
modifying or deleting files.
|
|
127
127
|
|
|
128
|
-
If you are running multiple MCP servers simultaneously, be
|
|
129
|
-
cautious
|
|
130
|
-
control, such as GitHub issues. These can become attack vectors. For more
|
|
128
|
+
If you are running multiple MCP servers simultaneously, be
|
|
129
|
+
cautious -- particularly if any server can access external resources you do not
|
|
130
|
+
control, such as GitHub issues. These can become attack vectors. For more
|
|
131
131
|
details, see [this story](https://simonwillison.net/2025/Jul/6/supabase-mcp-lethal-trifecta/).
|
|
132
132
|
|
|
133
133
|
## Implementation
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
in connection with [pythonanywhere-core](https://github.com/pythonanywhere/pythonanywhere-core)
|
|
137
|
-
package ([docs](https://core.pythonanywhere.com/))
|
|
138
|
-
API](https://help.pythonanywhere.com/pages/API/) and
|
|
135
|
+
The server uses the [python mcp sdk](https://github.com/modelcontextprotocol/python-sdk)
|
|
136
|
+
in connection with the [pythonanywhere-core](https://github.com/pythonanywhere/pythonanywhere-core)
|
|
137
|
+
package ([docs](https://core.pythonanywhere.com/)), which wraps a subset of the [PythonAnywhere
|
|
138
|
+
API](https://help.pythonanywhere.com/pages/API/) and may be expanded in
|
|
139
139
|
the future as needed.
|
|
@@ -1,41 +1,41 @@
|
|
|
1
1
|
# PythonAnywhere Model Context Protocol Server
|
|
2
2
|
|
|
3
|
-
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction)
|
|
4
|
-
server acts as a bridge between AI-powered tools and your
|
|
5
|
-
[PythonAnywhere](https://www.pythonanywhere.com/) account, enabling secure,
|
|
6
|
-
programmatic management of files, websites, webapps, and scheduled tasks. By
|
|
7
|
-
exposing a standardized interface, it allows language models and automation
|
|
8
|
-
clients to perform operations—such as editing files, deploying web apps, or
|
|
9
|
-
scheduling jobs
|
|
3
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction)
|
|
4
|
+
server acts as a bridge between AI-powered tools and your
|
|
5
|
+
[PythonAnywhere](https://www.pythonanywhere.com/) account, enabling secure,
|
|
6
|
+
programmatic management of files, websites, webapps, and scheduled tasks. By
|
|
7
|
+
exposing a standardized interface, it allows language models and automation
|
|
8
|
+
clients to perform operations—such as editing files, deploying web apps, or
|
|
9
|
+
scheduling jobs -- on your behalf, all while maintaining fine-grained control
|
|
10
10
|
and auditability.
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
|
-
- **File management**: Read, upload, delete files and list directory trees.
|
|
13
|
+
- **File management**: Read, upload, delete files and list directory trees.
|
|
14
14
|
_(also enables debugging with direct access to log files, which are just
|
|
15
15
|
files on PythonAnywhere)_
|
|
16
16
|
- **ASGI Web app management**: Create, delete, reload, and list.
|
|
17
|
-
_(as described in the [PythonAnywhere ASGI
|
|
17
|
+
_(as described in the [PythonAnywhere ASGI
|
|
18
18
|
documentation](https://help.pythonanywhere.com/pages/ASGICommandLine))_
|
|
19
19
|
- **WSGI Web app management**: Reload only _(at the moment)_.
|
|
20
20
|
- **Scheduled task management**: List, create, update, and delete.
|
|
21
|
-
_(
|
|
22
|
-
scheduled
|
|
21
|
+
_(Note that this enables LLMs to execute arbitrary commands if a task is
|
|
22
|
+
scheduled too soon after creation and deleted after execution. For that we
|
|
23
23
|
would suggest running it with [mcp-server-time](https://pypi.org/project/mcp-server-time/)
|
|
24
24
|
as models easily get confused about time.)_
|
|
25
25
|
|
|
26
26
|
## Installation
|
|
27
|
-
MCP protocol is well-defined and supported by various clients, but
|
|
28
|
-
installation is different depending on the client you are using. We will
|
|
27
|
+
The MCP protocol is well-defined and supported by various clients, but
|
|
28
|
+
installation is different depending on the client you are using. We will
|
|
29
29
|
cover cases that we tried and tested.
|
|
30
30
|
|
|
31
31
|
In all cases, you need to have `uv` installed and available in your `PATH`.
|
|
32
32
|
|
|
33
|
-
Have your PythonAnywhere API token and username ready. You can find (or
|
|
34
|
-
generate) your API token in the [API section of your PythonAnywhere
|
|
33
|
+
Have your PythonAnywhere API token and username ready. You can find (or
|
|
34
|
+
generate) your API token in the [API section of your PythonAnywhere
|
|
35
35
|
account](https://www.pythonanywhere.com/account/#api_token).
|
|
36
36
|
|
|
37
37
|
### Desktop Extension - works with Claude Desktop
|
|
38
|
-
Probably the most straightforward way to install the MCP server is to use
|
|
38
|
+
Probably the most straightforward way to install the MCP server is to use
|
|
39
39
|
the [desktop extension](https://github.com/anthropics/dxt/) for Claude Desktop.
|
|
40
40
|
|
|
41
41
|
1. Open Claude Desktop.
|
|
@@ -73,7 +73,7 @@ Add it to your `mcp.json`.
|
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
### Claude Desktop (manual setup) and Cursor:
|
|
76
|
-
Add it to `claude_desktop_config.json` (for Claude Desktop) or (`mcp.json`
|
|
76
|
+
Add it to `claude_desktop_config.json` (for Claude Desktop) or (`mcp.json`
|
|
77
77
|
for Cursor).
|
|
78
78
|
|
|
79
79
|
```json
|
|
@@ -94,20 +94,20 @@ for Cursor).
|
|
|
94
94
|
|
|
95
95
|
## Caveats
|
|
96
96
|
|
|
97
|
-
Direct integration of an LLM with your PythonAnywhere account offers
|
|
98
|
-
significant capabilities, but also introduces risks. We strongly advise
|
|
99
|
-
maintaining human oversight, especially for sensitive actions such as
|
|
97
|
+
Direct integration of an LLM with your PythonAnywhere account offers
|
|
98
|
+
significant capabilities, but also introduces risks. We strongly advise
|
|
99
|
+
maintaining human oversight, especially for sensitive actions such as
|
|
100
100
|
modifying or deleting files.
|
|
101
101
|
|
|
102
|
-
If you are running multiple MCP servers simultaneously, be
|
|
103
|
-
cautious
|
|
104
|
-
control, such as GitHub issues. These can become attack vectors. For more
|
|
102
|
+
If you are running multiple MCP servers simultaneously, be
|
|
103
|
+
cautious -- particularly if any server can access external resources you do not
|
|
104
|
+
control, such as GitHub issues. These can become attack vectors. For more
|
|
105
105
|
details, see [this story](https://simonwillison.net/2025/Jul/6/supabase-mcp-lethal-trifecta/).
|
|
106
106
|
|
|
107
107
|
## Implementation
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
in connection with [pythonanywhere-core](https://github.com/pythonanywhere/pythonanywhere-core)
|
|
111
|
-
package ([docs](https://core.pythonanywhere.com/))
|
|
112
|
-
API](https://help.pythonanywhere.com/pages/API/) and
|
|
113
|
-
the future as needed.
|
|
109
|
+
The server uses the [python mcp sdk](https://github.com/modelcontextprotocol/python-sdk)
|
|
110
|
+
in connection with the [pythonanywhere-core](https://github.com/pythonanywhere/pythonanywhere-core)
|
|
111
|
+
package ([docs](https://core.pythonanywhere.com/)), which wraps a subset of the [PythonAnywhere
|
|
112
|
+
API](https://help.pythonanywhere.com/pages/API/) and may be expanded in
|
|
113
|
+
the future as needed.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pythonanywhere-mcp-server"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.6"
|
|
8
8
|
description = "PythonAnywhere Model Context Protocol Server"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "PythonAnywhere Developers", email = "developers@pythonanywhere.com"}
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
|
|
8
|
+
# ToDo: Add the log file functions once pythonanywhere-core webapp log file functions
|
|
9
|
+
# have been improved
|
|
10
|
+
|
|
11
|
+
def register_webapp_tools(mcp: FastMCP) -> None:
|
|
12
|
+
@mcp.tool()
|
|
13
|
+
def reload_webapp(domain: str) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Reload a uWSGI-based web application for the given domain.
|
|
16
|
+
|
|
17
|
+
This reloads uwsgi-based web applications on PythonAnywhere. For ASGI-based
|
|
18
|
+
web applications, use the `reload_website` tool instead. Any changes to the
|
|
19
|
+
code require a reload to take effect.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
domain (str): The domain name of the web application to reload
|
|
23
|
+
(e.g., 'alice.pythonanywhere.com').
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
str: Status message indicating reload's result.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
Webapp(domain).reload()
|
|
30
|
+
return f"Webapp '{domain}' reloaded."
|
|
31
|
+
except (AuthenticationError, NoTokenError):
|
|
32
|
+
raise RuntimeError("Authentication failed — check API_TOKEN and domain.")
|
|
33
|
+
except Exception as exc:
|
|
34
|
+
raise RuntimeError(str(exc)) from exc
|
|
35
|
+
|
|
36
|
+
@mcp.tool()
|
|
37
|
+
def create_webapp(domain: str, python_version: str, virtualenv_path: str, project_path: str) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Create a new uWSGI-based web application for the given domain.
|
|
40
|
+
|
|
41
|
+
This creates a new webapp on PythonAnywhere with the specified configuration.
|
|
42
|
+
The webapp will be created using the specified Python version, virtual environment,
|
|
43
|
+
and project path. If a webapp already exists for this domain, it will fail unless
|
|
44
|
+
the nuke parameter is set to True.
|
|
45
|
+
|
|
46
|
+
The WSGI configuration file can be found in /var/www. The file is of the format:
|
|
47
|
+
domain.replace('.', '_') + '_wsgi.py' It would be automatically created as side effect of running this tool.
|
|
48
|
+
|
|
49
|
+
Note:
|
|
50
|
+
Virtual environments should be configured in the PythonAnywhere web app
|
|
51
|
+
configuration, not in the WSGI file itself.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
domain (str): The domain name for the new webapp (e.g., 'alice.pythonanywhere.com').
|
|
55
|
+
python_version (str): Python version to use (e.g., '3.11', '3.10', '3.9').
|
|
56
|
+
virtualenv_path (str | None): Path to the virtual environment to use. (or None for
|
|
57
|
+
no virtualenv when using one of pre-installed Pythons with batteries included packages)
|
|
58
|
+
project_path (str): Path to the project directory containing the webapp code.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
str: Status message indicating creation result.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
RuntimeError: If authentication fails, webapp already exists (and nuke=False),
|
|
65
|
+
or other API errors occur. If 403 error is raised, it may mean that there
|
|
66
|
+
is already a non-uwsgi-based website. `list_websites` tool can be used to check that.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
webapp = Webapp(domain)
|
|
70
|
+
webapp.create(
|
|
71
|
+
python_version=python_version,
|
|
72
|
+
virtualenv_path=Path(virtualenv_path),
|
|
73
|
+
project_path=Path(project_path),
|
|
74
|
+
nuke=False
|
|
75
|
+
)
|
|
76
|
+
return f"Webapp '{domain}' created successfully."
|
|
77
|
+
except (AuthenticationError, NoTokenError):
|
|
78
|
+
raise RuntimeError("Authentication failed — check API_TOKEN and domain.")
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
raise RuntimeError(str(exc)) from exc
|
|
81
|
+
|
|
82
|
+
@mcp.tool()
|
|
83
|
+
def delete_webapp(domain: str) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Delete a uWSGI-based web application for the given domain.
|
|
86
|
+
|
|
87
|
+
This permanently deletes the webapp configuration from PythonAnywhere. The actual
|
|
88
|
+
files in your file system are not deleted, only the webapp configuration that
|
|
89
|
+
serves them. This action cannot be undone.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
domain (str): The domain name of the webapp to delete
|
|
93
|
+
(e.g., 'alice.pythonanywhere.com').
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
str: Status message indicating deletion result.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
RuntimeError: If authentication fails, webapp doesn't exist, or other API errors occur.
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
Webapp(domain).delete()
|
|
103
|
+
return f"Webapp '{domain}' deleted successfully."
|
|
104
|
+
except (AuthenticationError, NoTokenError):
|
|
105
|
+
raise RuntimeError("Authentication failed — check API_TOKEN and domain.")
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
raise RuntimeError(str(exc)) from exc
|
|
108
|
+
|
|
109
|
+
@mcp.tool()
|
|
110
|
+
def patch_webapp(domain: str, data: dict) -> dict:
|
|
111
|
+
"""
|
|
112
|
+
Update configuration settings for a uWSGI-based web application.
|
|
113
|
+
|
|
114
|
+
This allows you to modify various webapp settings such as the Python version,
|
|
115
|
+
virtual environment path, source directory, and other configuration options.
|
|
116
|
+
Only the fields provided in the data dictionary will be updated.
|
|
117
|
+
|
|
118
|
+
In order for any changes to take effect you must reload the webapp.
|
|
119
|
+
|
|
120
|
+
If you provide an invalid Python version you will receive a 400 error with an error
|
|
121
|
+
message that the version you used is not a valid choice. You will need to choose a
|
|
122
|
+
different Python version if you get this error.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
domain (str): The domain name of the webapp to update
|
|
126
|
+
(e.g., 'alice.pythonanywhere.com').
|
|
127
|
+
data (dict): Dictionary containing the configuration updates. Supported keys are:
|
|
128
|
+
- 'python_version': Python version (e.g., '3.11')
|
|
129
|
+
- 'virtualenv_path': Path to virtual environment
|
|
130
|
+
- 'source_directory': Path to source code directory
|
|
131
|
+
- 'working_directory': Working directory for the webapp
|
|
132
|
+
- 'force_https': Force the use of HTTPS when accessing the webapp
|
|
133
|
+
- 'password_protection_enabled': Enable basic HTTP password to your webapp, provided via PythonAnywhere not in the webapp code
|
|
134
|
+
- 'password_protection_username': The username used for HTTP password protection
|
|
135
|
+
- 'password_protection_password': The password used for HTTP password protection
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
dict: Updated webapp configuration information.
|
|
139
|
+
Example: {
|
|
140
|
+
"id": 2097234,
|
|
141
|
+
"user": username,
|
|
142
|
+
"domain_name": domain,
|
|
143
|
+
"python_version": "3.10",
|
|
144
|
+
"source_directory": f"/home/{username}/mysite",
|
|
145
|
+
"working_directory": f"/home/{username}/",
|
|
146
|
+
"virtualenv_path": "",
|
|
147
|
+
"expiry": "2025-10-16",
|
|
148
|
+
"force_https": False,
|
|
149
|
+
"password_protection_enabled": False,
|
|
150
|
+
"password_protection_username": "foo",
|
|
151
|
+
"password_protection_password": "bar"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
RuntimeError: If authentication fails, webapp doesn't exist, or other API errors occur.
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
result = Webapp(domain).patch(data)
|
|
159
|
+
return result
|
|
160
|
+
except (AuthenticationError, NoTokenError):
|
|
161
|
+
raise RuntimeError("Authentication failed — check API_TOKEN and domain.")
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
raise RuntimeError(str(exc)) from exc
|
|
164
|
+
|
|
165
|
+
@mcp.tool()
|
|
166
|
+
def list_webapps() -> list:
|
|
167
|
+
"""
|
|
168
|
+
List all uWSGI-based web applications for the current user.
|
|
169
|
+
|
|
170
|
+
This retrieves information about all webapps configured in your PythonAnywhere
|
|
171
|
+
account. The returned list contains dictionaries with detailed information about
|
|
172
|
+
each webapp including domain, Python version, paths, and status.
|
|
173
|
+
|
|
174
|
+
On PythonAnywhere one may also have non-uWSGI-based websites (usually ASGI-based),
|
|
175
|
+
which are not included in this list. For those, use the `list_websites` tool.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
list: List of dictionaries containing webapp information. Empty list means
|
|
179
|
+
no WSGI-based webapps are deployed. That still could mean that there are
|
|
180
|
+
non-WSGI-based apps that can be listed with the `list_websites` tool.
|
|
181
|
+
|
|
182
|
+
See also:
|
|
183
|
+
get_webapp_info: Get detailed information about a specific webapp.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
RuntimeError: If authentication fails or other API errors occur.
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
result = Webapp.list_webapps()
|
|
190
|
+
return result
|
|
191
|
+
except (AuthenticationError, NoTokenError):
|
|
192
|
+
raise RuntimeError("Authentication failed — check API_TOKEN.")
|
|
193
|
+
except Exception as exc:
|
|
194
|
+
raise RuntimeError(str(exc)) from exc
|
|
195
|
+
|
|
196
|
+
@mcp.tool()
|
|
197
|
+
def get_webapp_info(domain: str) -> dict:
|
|
198
|
+
"""
|
|
199
|
+
Get detailed information about a specific uWSGI-based web application.
|
|
200
|
+
|
|
201
|
+
This retrieves comprehensive configuration and status information for the
|
|
202
|
+
specified webapp, including paths, Python version, enabled status, and
|
|
203
|
+
other configuration details.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
domain (str): The domain name of the webapp to get information for
|
|
207
|
+
(e.g., 'alice.pythonanywhere.com').
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
dict: Dictionary containing detailed webapp information including:
|
|
211
|
+
- "id": int, # Unique identifier for the site or user session
|
|
212
|
+
- "user": str, # Username associated with the deployment
|
|
213
|
+
- "domain_name": str, # Domain name for the deployed site
|
|
214
|
+
- "python_version": str, # Python version used, e.g., "3.10"
|
|
215
|
+
- "source_directory": str, # Absolute path to the site's source directory
|
|
216
|
+
- "working_directory": str, # Absolute path to the working directory
|
|
217
|
+
- "virtualenv_path": str, # Path to the Python virtual environment (can be empty)
|
|
218
|
+
- "expiry": str, # Expiration date in ISO format, e.g., "2025-10-16"
|
|
219
|
+
- "force_https": bool, # Whether HTTPS is enforced
|
|
220
|
+
- "password_protection_enabled": bool, # Whether password protection is enabled
|
|
221
|
+
- "password_protection_username": str, # Username for password-protected access
|
|
222
|
+
- "password_protection_password": str # Password for password-protected access
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
RuntimeError: If authentication fails, webapp doesn't exist, or other API errors occur.
|
|
227
|
+
"""
|
|
228
|
+
try:
|
|
229
|
+
result = Webapp(domain).get()
|
|
230
|
+
return result
|
|
231
|
+
except (AuthenticationError, NoTokenError):
|
|
232
|
+
raise RuntimeError("Authentication failed — check API_TOKEN and domain.")
|
|
233
|
+
except Exception as exc:
|
|
234
|
+
raise RuntimeError(str(exc)) from exc
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
@@ -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
|
|
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.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: PythonAnywhere Model Context Protocol Server
|
|
5
5
|
Author-email: PythonAnywhere Developers <developers@pythonanywhere.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,42 +26,42 @@ Dynamic: license-file
|
|
|
26
26
|
|
|
27
27
|
# PythonAnywhere Model Context Protocol Server
|
|
28
28
|
|
|
29
|
-
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction)
|
|
30
|
-
server acts as a bridge between AI-powered tools and your
|
|
31
|
-
[PythonAnywhere](https://www.pythonanywhere.com/) account, enabling secure,
|
|
32
|
-
programmatic management of files, websites, webapps, and scheduled tasks. By
|
|
33
|
-
exposing a standardized interface, it allows language models and automation
|
|
34
|
-
clients to perform operations—such as editing files, deploying web apps, or
|
|
35
|
-
scheduling jobs
|
|
29
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction)
|
|
30
|
+
server acts as a bridge between AI-powered tools and your
|
|
31
|
+
[PythonAnywhere](https://www.pythonanywhere.com/) account, enabling secure,
|
|
32
|
+
programmatic management of files, websites, webapps, and scheduled tasks. By
|
|
33
|
+
exposing a standardized interface, it allows language models and automation
|
|
34
|
+
clients to perform operations—such as editing files, deploying web apps, or
|
|
35
|
+
scheduling jobs -- on your behalf, all while maintaining fine-grained control
|
|
36
36
|
and auditability.
|
|
37
37
|
|
|
38
38
|
## Features
|
|
39
|
-
- **File management**: Read, upload, delete files and list directory trees.
|
|
39
|
+
- **File management**: Read, upload, delete files and list directory trees.
|
|
40
40
|
_(also enables debugging with direct access to log files, which are just
|
|
41
41
|
files on PythonAnywhere)_
|
|
42
42
|
- **ASGI Web app management**: Create, delete, reload, and list.
|
|
43
|
-
_(as described in the [PythonAnywhere ASGI
|
|
43
|
+
_(as described in the [PythonAnywhere ASGI
|
|
44
44
|
documentation](https://help.pythonanywhere.com/pages/ASGICommandLine))_
|
|
45
45
|
- **WSGI Web app management**: Reload only _(at the moment)_.
|
|
46
46
|
- **Scheduled task management**: List, create, update, and delete.
|
|
47
|
-
_(
|
|
48
|
-
scheduled
|
|
47
|
+
_(Note that this enables LLMs to execute arbitrary commands if a task is
|
|
48
|
+
scheduled too soon after creation and deleted after execution. For that we
|
|
49
49
|
would suggest running it with [mcp-server-time](https://pypi.org/project/mcp-server-time/)
|
|
50
50
|
as models easily get confused about time.)_
|
|
51
51
|
|
|
52
52
|
## Installation
|
|
53
|
-
MCP protocol is well-defined and supported by various clients, but
|
|
54
|
-
installation is different depending on the client you are using. We will
|
|
53
|
+
The MCP protocol is well-defined and supported by various clients, but
|
|
54
|
+
installation is different depending on the client you are using. We will
|
|
55
55
|
cover cases that we tried and tested.
|
|
56
56
|
|
|
57
57
|
In all cases, you need to have `uv` installed and available in your `PATH`.
|
|
58
58
|
|
|
59
|
-
Have your PythonAnywhere API token and username ready. You can find (or
|
|
60
|
-
generate) your API token in the [API section of your PythonAnywhere
|
|
59
|
+
Have your PythonAnywhere API token and username ready. You can find (or
|
|
60
|
+
generate) your API token in the [API section of your PythonAnywhere
|
|
61
61
|
account](https://www.pythonanywhere.com/account/#api_token).
|
|
62
62
|
|
|
63
63
|
### Desktop Extension - works with Claude Desktop
|
|
64
|
-
Probably the most straightforward way to install the MCP server is to use
|
|
64
|
+
Probably the most straightforward way to install the MCP server is to use
|
|
65
65
|
the [desktop extension](https://github.com/anthropics/dxt/) for Claude Desktop.
|
|
66
66
|
|
|
67
67
|
1. Open Claude Desktop.
|
|
@@ -99,7 +99,7 @@ Add it to your `mcp.json`.
|
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
### Claude Desktop (manual setup) and Cursor:
|
|
102
|
-
Add it to `claude_desktop_config.json` (for Claude Desktop) or (`mcp.json`
|
|
102
|
+
Add it to `claude_desktop_config.json` (for Claude Desktop) or (`mcp.json`
|
|
103
103
|
for Cursor).
|
|
104
104
|
|
|
105
105
|
```json
|
|
@@ -120,20 +120,20 @@ for Cursor).
|
|
|
120
120
|
|
|
121
121
|
## Caveats
|
|
122
122
|
|
|
123
|
-
Direct integration of an LLM with your PythonAnywhere account offers
|
|
124
|
-
significant capabilities, but also introduces risks. We strongly advise
|
|
125
|
-
maintaining human oversight, especially for sensitive actions such as
|
|
123
|
+
Direct integration of an LLM with your PythonAnywhere account offers
|
|
124
|
+
significant capabilities, but also introduces risks. We strongly advise
|
|
125
|
+
maintaining human oversight, especially for sensitive actions such as
|
|
126
126
|
modifying or deleting files.
|
|
127
127
|
|
|
128
|
-
If you are running multiple MCP servers simultaneously, be
|
|
129
|
-
cautious
|
|
130
|
-
control, such as GitHub issues. These can become attack vectors. For more
|
|
128
|
+
If you are running multiple MCP servers simultaneously, be
|
|
129
|
+
cautious -- particularly if any server can access external resources you do not
|
|
130
|
+
control, such as GitHub issues. These can become attack vectors. For more
|
|
131
131
|
details, see [this story](https://simonwillison.net/2025/Jul/6/supabase-mcp-lethal-trifecta/).
|
|
132
132
|
|
|
133
133
|
## Implementation
|
|
134
134
|
|
|
135
|
-
|
|
136
|
-
in connection with [pythonanywhere-core](https://github.com/pythonanywhere/pythonanywhere-core)
|
|
137
|
-
package ([docs](https://core.pythonanywhere.com/))
|
|
138
|
-
API](https://help.pythonanywhere.com/pages/API/) and
|
|
135
|
+
The server uses the [python mcp sdk](https://github.com/modelcontextprotocol/python-sdk)
|
|
136
|
+
in connection with the [pythonanywhere-core](https://github.com/pythonanywhere/pythonanywhere-core)
|
|
137
|
+
package ([docs](https://core.pythonanywhere.com/)), which wraps a subset of the [PythonAnywhere
|
|
138
|
+
API](https://help.pythonanywhere.com/pages/API/) and may be expanded in
|
|
139
139
|
the future as needed.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from pythonanywhere_core.base import AuthenticationError
|
|
4
|
+
|
|
5
|
+
import tools.webapp as webapp_tools
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture
|
|
9
|
+
def setup_webapp_tools(mcp):
|
|
10
|
+
webapp_tools.register_webapp_tools(mcp)
|
|
11
|
+
return mcp
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.parametrize("tool_name,method_name,params,expected_params,expected_result", [
|
|
15
|
+
("reload_webapp", "reload", {"domain": "test.com"}, {}, "Webapp 'test.com' reloaded."),
|
|
16
|
+
("delete_webapp", "delete", {"domain": "test.com"}, {}, "Webapp 'test.com' deleted successfully."),
|
|
17
|
+
("get_webapp_info", "get", {"domain": "test.com"}, {}, {"domain_name": "test.com", "python_version": "3.10"}),
|
|
18
|
+
("patch_webapp", "patch", {"domain": "test.com", "data": {"python_version": "3.10"}}, {"python_version": "3.10"}, {"domain_name": "test.com", "python_version": "3.10"}),
|
|
19
|
+
])
|
|
20
|
+
def test_webapp_tools_success(setup_webapp_tools, mocker, tool_name, method_name, params, expected_params, expected_result):
|
|
21
|
+
mock_webapp = mocker.patch("tools.webapp.Webapp", autospec=True)
|
|
22
|
+
|
|
23
|
+
if tool_name == "get_webapp_info" or tool_name == "patch_webapp":
|
|
24
|
+
getattr(mock_webapp.return_value, method_name).return_value = expected_result
|
|
25
|
+
|
|
26
|
+
result = setup_webapp_tools.call_tool(tool_name, params)
|
|
27
|
+
|
|
28
|
+
if "domain" in params:
|
|
29
|
+
mock_webapp.assert_called_with(params["domain"])
|
|
30
|
+
if expected_params:
|
|
31
|
+
getattr(mock_webapp.return_value, method_name).assert_called_with(expected_params)
|
|
32
|
+
else:
|
|
33
|
+
getattr(mock_webapp.return_value, method_name).assert_called_once()
|
|
34
|
+
|
|
35
|
+
assert result == expected_result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize("tool_name,method_name,params,side_effect,expected_error", [
|
|
39
|
+
("reload_webapp", "reload", {"domain": "test.com"}, AuthenticationError(), "Authentication failed"),
|
|
40
|
+
("reload_webapp", "reload", {"domain": "test.com"}, Exception("webapp reload error"), "webapp reload error"),
|
|
41
|
+
("delete_webapp", "delete", {"domain": "test.com"}, AuthenticationError(), "Authentication failed"),
|
|
42
|
+
("delete_webapp", "delete", {"domain": "test.com"}, Exception("delete error"), "delete error"),
|
|
43
|
+
("get_webapp_info", "get", {"domain": "test.com"}, AuthenticationError(), "Authentication failed"),
|
|
44
|
+
("get_webapp_info", "get", {"domain": "test.com"}, Exception("info error"), "info error"),
|
|
45
|
+
("patch_webapp", "patch", {"domain": "test.com", "data": {"python_version": "3.10"}}, AuthenticationError(), "Authentication failed"),
|
|
46
|
+
("patch_webapp", "patch", {"domain": "test.com", "data": {"python_version": "3.10"}}, Exception("patch error"), "patch error"),
|
|
47
|
+
])
|
|
48
|
+
def test_webapp_tools_errors(setup_webapp_tools, mocker, tool_name, method_name, params, side_effect, expected_error):
|
|
49
|
+
mock_webapp = mocker.patch("tools.webapp.Webapp", autospec=True)
|
|
50
|
+
getattr(mock_webapp.return_value, method_name).side_effect = side_effect
|
|
51
|
+
|
|
52
|
+
with pytest.raises(RuntimeError) as exc:
|
|
53
|
+
setup_webapp_tools.call_tool(tool_name, params)
|
|
54
|
+
assert expected_error in str(exc)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize("side_effect,expected_error", [
|
|
58
|
+
(AuthenticationError(), "Authentication failed"),
|
|
59
|
+
(Exception("list error"), "list error"),
|
|
60
|
+
])
|
|
61
|
+
def test_list_webapps_errors(setup_webapp_tools, mocker, side_effect, expected_error):
|
|
62
|
+
mocker.patch("tools.webapp.Webapp", autospec=True)
|
|
63
|
+
mocker.patch("tools.webapp.Webapp.list_webapps", side_effect=side_effect)
|
|
64
|
+
|
|
65
|
+
with pytest.raises(RuntimeError) as exc:
|
|
66
|
+
setup_webapp_tools.call_tool("list_webapps", {})
|
|
67
|
+
assert expected_error in str(exc)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_create_webapp(setup_webapp_tools, mocker):
|
|
71
|
+
webapp_domain = 'test.com'
|
|
72
|
+
python_version = '3.10'
|
|
73
|
+
virtualenv_path = Path('/test/venv/path')
|
|
74
|
+
project_path = Path('/project/path')
|
|
75
|
+
|
|
76
|
+
mock_webapp = mocker.patch("tools.webapp.Webapp", autospec=True)
|
|
77
|
+
result = setup_webapp_tools.call_tool(
|
|
78
|
+
"create_webapp",
|
|
79
|
+
{
|
|
80
|
+
"domain": webapp_domain,
|
|
81
|
+
"python_version": python_version,
|
|
82
|
+
"virtualenv_path": virtualenv_path,
|
|
83
|
+
"project_path": project_path,
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
mock_webapp.assert_called_with(webapp_domain)
|
|
87
|
+
mock_webapp.return_value.create.assert_called_with(
|
|
88
|
+
python_version=python_version,
|
|
89
|
+
virtualenv_path=virtualenv_path,
|
|
90
|
+
project_path=project_path,
|
|
91
|
+
nuke=False
|
|
92
|
+
)
|
|
93
|
+
mock_webapp.return_value.create.assert_called_once()
|
|
94
|
+
assert result == f"Webapp 'test.com' created successfully."
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.parametrize("side_effect,expected_error", [
|
|
98
|
+
(AuthenticationError(), "Authentication failed"),
|
|
99
|
+
(Exception("webapp create error"), "webapp create error"),
|
|
100
|
+
])
|
|
101
|
+
def test_create_webapp_errors(setup_webapp_tools, mocker, side_effect, expected_error):
|
|
102
|
+
mock_webapp = mocker.patch("tools.webapp.Webapp", autospec=True)
|
|
103
|
+
mock_webapp.return_value.create.side_effect = side_effect
|
|
104
|
+
params = {
|
|
105
|
+
"domain": "test.com",
|
|
106
|
+
"python_version": "3.10",
|
|
107
|
+
"virtualenv_path": "/test/venv/path",
|
|
108
|
+
"project_path": "/project/path",
|
|
109
|
+
}
|
|
110
|
+
with pytest.raises(RuntimeError) as exc:
|
|
111
|
+
setup_webapp_tools.call_tool("create_webapp", params)
|
|
112
|
+
assert expected_error in str(exc)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_list_webapps(setup_webapp_tools, mocker):
|
|
116
|
+
mocker.patch("tools.webapp.Webapp", autospec=True)
|
|
117
|
+
expected = [{"domain_name": "test.com", "python_version": "3.10"}]
|
|
118
|
+
mocker.patch("tools.webapp.Webapp.list_webapps", return_value=expected)
|
|
119
|
+
result = setup_webapp_tools.call_tool("list_webapps", {})
|
|
120
|
+
assert result == expected
|
|
@@ -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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/tests/test_file_tools.py
RENAMED
|
File without changes
|
{pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/tests/test_schedule_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
{pythonanywhere_mcp_server-0.0.4 → pythonanywhere_mcp_server-0.0.6}/tests/test_website_tools.py
RENAMED
|
File without changes
|