Skip to content

Commit 57a57bf

Browse files
filiplajszczakcaseneuveleec1979
committed
initial commit
Co-authored-by: Piotr Kaznowski <[email protected]> Co-authored-by: Lee Cartwright <[email protected]>
0 parents  commit 57a57bf

25 files changed

+1604
-0
lines changed

.dxtignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*
2+
3+
!manifest.json
4+
!icon.png

.github/workflows/release.yml

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*.*.*'
7+
workflow_dispatch:
8+
9+
jobs:
10+
test:
11+
uses: ./.github/workflows/test.yml
12+
13+
build_package:
14+
runs-on: ubuntu-latest
15+
needs: test
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: '3.13'
24+
25+
- name: Install uv
26+
uses: astral-sh/setup-uv@v3
27+
28+
- name: Check tag matches pyproject.toml version
29+
run: |
30+
TAG_VERSION="${GITHUB_REF##*/}"
31+
TAG_VERSION_NO_PREFIX="${TAG_VERSION#v}"
32+
echo "Tag version: $TAG_VERSION (stripped: $TAG_VERSION_NO_PREFIX)"
33+
PYPROJECT_VERSION=$(grep '^version =' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
34+
echo "pyproject.toml version: $PYPROJECT_VERSION"
35+
if [ "$TAG_VERSION_NO_PREFIX" != "$PYPROJECT_VERSION" ]; then
36+
echo "Tag version ($TAG_VERSION_NO_PREFIX) does not match pyproject.toml version ($PYPROJECT_VERSION)" >&2
37+
exit 1
38+
fi
39+
shell: bash
40+
41+
- name: Build package
42+
run: uv build
43+
44+
- name: Publish to PyPI
45+
uses: pypa/gh-action-pypi-publish@release/v1
46+
with:
47+
user: __token__
48+
password: ${{ secrets.PYPI_API_TOKEN }}
49+
50+
- name: Upload Python artifacts
51+
uses: actions/upload-artifact@v4
52+
with:
53+
name: python-dist
54+
path: dist/
55+
56+
build_extension:
57+
runs-on: ubuntu-latest
58+
needs: test
59+
steps:
60+
- name: Checkout code
61+
uses: actions/checkout@v4
62+
63+
- name: Set up Node.js
64+
uses: actions/setup-node@v4
65+
with:
66+
node-version: '18'
67+
68+
- name: Check tag matches manifest.json version
69+
run: |
70+
TAG_VERSION="${GITHUB_REF##*/}"
71+
TAG_VERSION_NO_PREFIX="${TAG_VERSION#v}"
72+
echo "Tag version: $TAG_VERSION (stripped: $TAG_VERSION_NO_PREFIX)"
73+
MANIFEST_VERSION=$(jq -r .version manifest.json)
74+
echo "manifest.json version: $MANIFEST_VERSION"
75+
if [ "$TAG_VERSION_NO_PREFIX" != "$MANIFEST_VERSION" ]; then
76+
echo "Tag version ($TAG_VERSION_NO_PREFIX) does not match manifest.json version ($MANIFEST_VERSION)" >&2
77+
exit 1
78+
fi
79+
shell: bash
80+
81+
- name: Install DXT toolchain
82+
run: npm install -g @anthropic-ai/dxt
83+
84+
- name: Pack extension
85+
run: dxt pack
86+
87+
- name: Upload DXT artifacts
88+
uses: actions/upload-artifact@v4
89+
with:
90+
name: dxt-dist
91+
path: '*.dxt'
92+
93+
create_release:
94+
runs-on: ubuntu-latest
95+
needs: [build_package, build_extension]
96+
steps:
97+
- name: Download Python artifacts
98+
uses: actions/download-artifact@v4
99+
with:
100+
name: python-dist
101+
path: dist/
102+
103+
- name: Download DXT artifacts
104+
uses: actions/download-artifact@v4
105+
with:
106+
name: dxt-dist
107+
path: ./
108+
109+
- name: Create Release
110+
uses: softprops/action-gh-release@v1
111+
with:
112+
files: |
113+
*.dxt
114+
dist/*
115+
env:
116+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/test.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
branches: [ master ]
8+
workflow_call:
9+
10+
jobs:
11+
test:
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
python-version: ["3.13"]
16+
steps:
17+
- uses: actions/checkout@v4
18+
- name: Install the latest version of uv and set the python version
19+
uses: astral-sh/setup-uv@v6
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
activate-environment: true
23+
- name: Install dependencies
24+
run: uv pip install -r pyproject.toml
25+
- name: Install test dependencies
26+
run: uv pip install ".[test]"
27+
- name: Test with python ${{ matrix.python-version }}
28+
run: uv run --frozen pytest

.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Python-generated files
2+
__pycache__/
3+
*.py[oc]
4+
build/
5+
dist/
6+
wheels/
7+
*.egg-info
8+
9+
# Desktop Extension Artifacts
10+
*.dxt
11+
12+
# Virtual environments
13+
.venv
14+
15+
# Pytest cache
16+
.pytest_cache/
17+
18+
# Intellij IDEA files
19+
.idea

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2025 PythonAnywhere
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+

README.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# PythonAnywhere Model Context Protocol Server
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—on your behalf, all while maintaining fine-grained control
10+
and auditability.
11+
12+
## Features
13+
- **File management**: Read, upload, delete files and list directory trees.
14+
_(also enables debugging with direct access to log files, which are just
15+
files on PythonAnywhere)_
16+
- **ASGI Web app management**: Create, delete, reload, and list.
17+
_(as described in the [PythonAnywhere ASGI
18+
documentation](https://help.pythonanywhere.com/pages/ASGICommandLine))_
19+
- **WSGI Web app management**: Reload only _(at the moment)_.
20+
- **Scheduled task management**: List, create, update, and delete.
21+
_(Npote that it enables LLMs to execute arbitrary commands if a task is
22+
scheduled to soon after creation and deleted after execution. For that we
23+
would suggest running it with [mcp-server-time](https://pypi.org/project/mcp-server-time/)
24+
as models easily get confused about time.)_
25+
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
29+
cover cases that we tried and tested.
30+
31+
In all cases, you need to have `uv` installed and available in your `PATH`.
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
35+
account](https://www.pythonanywhere.com/account/#api_token).
36+
37+
### Desktop Extension - works with Claude Desktop
38+
Probably the most straightforward way to install the MCP server is to use
39+
the [desktop extension](https://github.com/anthropics/dxt/) for Claude Desktop.
40+
41+
1. Open Claude Desktop.
42+
2. **[Download the latest .dxt file](https://github.com/pythonanywhere/pythonanywhere-mcp-server/releases/latest/download/pythonanywhere-mcp-server.dxt)**.
43+
3. Double-click on the downloaded .dxt file or drag the file into the window.
44+
4. Configure your PythonAnywhere API token and username.
45+
5. Restart Claude Desktop.
46+
47+
### Claude Code
48+
Run:
49+
```bash
50+
claude mcp add pythonanywhere-mcp-server\
51+
-e API_TOKEN=yourpythonanywhereapitoken\
52+
-e LOGNAME=yourpythonanywhereusername\
53+
-- uvx pythonanywhere-mcp-server
54+
```
55+
56+
### GitHub Copilot in PyCharm:
57+
Add it to your `mcp.json`.
58+
59+
```json
60+
{
61+
"servers": {
62+
"pythonanywhere-mcp-server": {
63+
"type": "stdio",
64+
"command": "uvx",
65+
"args": ["pythonanywhere-mcp-server"],
66+
"env": {
67+
"API_TOKEN": "yourpythonanywhereapitoken",
68+
"LOGNAME": "yourpythonanywhereusername"
69+
}
70+
}
71+
}
72+
}
73+
```
74+
75+
### Claude Desktop (manual setup) and Cursor:
76+
Add it to `claude_desktop_config.json` (for Claude Desktop) or (`mcp.json`
77+
for Cursor).
78+
79+
```json
80+
{
81+
"mcpServers": {
82+
"pythonanywhere-mcp-server": {
83+
"type": "stdio",
84+
"command": "uvx",
85+
"args": ["pythonanywhere-mcp-server"],
86+
"env": {
87+
"API_TOKEN": "yourpythonanywhereapitoken",
88+
"LOGNAME": "yourpythonanywhereusername"
89+
}
90+
}
91+
}
92+
}
93+
```
94+
95+
## Caveats
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
100+
modifying or deleting files.
101+
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+
details, see [this story](https://simonwillison.net/2025/Jul/6/supabase-mcp-lethal-trifecta/).
106+
107+
## Implementation
108+
109+
Server uses [python mcp sdk](https://github.com/modelcontextprotocol/python-sdk)
110+
in connection with [pythonanywhere-core](https://github.com/pythonanywhere/pythonanywhere-core)
111+
package ([docs](https://core.pythonanywhere.com/)) that wraps subset of [PythonAnywhere
112+
API](https://help.pythonanywhere.com/pages/API/) and would be expanded in
113+
the future as needed.

icon.png

18.7 KB
Loading

manifest.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"dxt_version": "0.1",
3+
"name": "PythonAnywhere MCP Server",
4+
"description": "Manage files, websites, and scheduled tasks on PythonAnywhere via the Model Context Protocol.",
5+
"icon": "icon.png",
6+
"version": "0.0.1",
7+
"author": {
8+
"name": "PythonAnywhere Developers",
9+
"email": "[email protected]"
10+
},
11+
"main": "pythonanywhere_mcp_server.py",
12+
"categories": [
13+
"developer-tools",
14+
"utilities"
15+
],
16+
"license": "MIT",
17+
"permissions": [
18+
"mcp",
19+
"filesystem"
20+
],
21+
"user_config": {
22+
"pa_api_token": {
23+
"type": "string",
24+
"title": "PythonAnywhere API Token",
25+
"description": "Your PythonAnywhere API token",
26+
"required": true
27+
},
28+
"pa_username": {
29+
"type": "string",
30+
"title": "PythonAnywhere Username",
31+
"description": "Your PythonAnywhere username",
32+
"required": true
33+
}
34+
},
35+
"server": {
36+
"type": "binary",
37+
"entry_point": "pythonanywhere_mcp_server.py",
38+
"mcp_config": {
39+
"command": "uvx",
40+
"args": ["pythonanywhere-mcp-server"],
41+
"env": {
42+
"API_TOKEN": "${user_config.pa_api_token}",
43+
"LOGNAME": "${user_config.pa_username}"
44+
}
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)