pestrail 0.1.0__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.
- pestrail-0.1.0/.env.example +25 -0
- pestrail-0.1.0/.gitignore +23 -0
- pestrail-0.1.0/LICENSE +21 -0
- pestrail-0.1.0/PKG-INFO +137 -0
- pestrail-0.1.0/README.md +109 -0
- pestrail-0.1.0/pyproject.toml +73 -0
- pestrail-0.1.0/src/pestrail/__init__.py +3 -0
- pestrail-0.1.0/src/pestrail/__main__.py +90 -0
- pestrail-0.1.0/src/pestrail/attachments.py +96 -0
- pestrail-0.1.0/src/pestrail/client.py +541 -0
- pestrail-0.1.0/src/pestrail/config.py +315 -0
- pestrail-0.1.0/src/pestrail/ids.py +130 -0
- pestrail-0.1.0/src/pestrail/read_tools.py +787 -0
- pestrail-0.1.0/src/pestrail/scope.py +163 -0
- pestrail-0.1.0/src/pestrail/server.py +129 -0
- pestrail-0.1.0/src/pestrail/shaping.py +169 -0
- pestrail-0.1.0/src/pestrail/write_tools.py +540 -0
- pestrail-0.1.0/tests/__init__.py +0 -0
- pestrail-0.1.0/tests/conftest.py +65 -0
- pestrail-0.1.0/tests/test_attachments.py +131 -0
- pestrail-0.1.0/tests/test_client.py +636 -0
- pestrail-0.1.0/tests/test_config.py +330 -0
- pestrail-0.1.0/tests/test_email.py +36 -0
- pestrail-0.1.0/tests/test_ids.py +87 -0
- pestrail-0.1.0/tests/test_scope.py +217 -0
- pestrail-0.1.0/tests/test_server.py +628 -0
- pestrail-0.1.0/tests/test_shaping.py +20 -0
- pestrail-0.1.0/tests/test_stdio_smoke.py +228 -0
- pestrail-0.1.0/tests/test_timestamps.py +34 -0
- pestrail-0.1.0/tests/test_write_payloads.py +98 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copy to your per-user pestrail config and fill in. Never commit it.
|
|
2
|
+
# Windows: %APPDATA%\pestrail\.env
|
|
3
|
+
# macOS / Linux: ~/.config/pestrail/.env (or $XDG_CONFIG_HOME/pestrail/.env)
|
|
4
|
+
# A .env in the working directory is NOT loaded automatically; point PESTRAIL_ENV_FILE at it
|
|
5
|
+
# to use one explicitly. Real environment variables (e.g. MCP client config) always win.
|
|
6
|
+
# Only TESTRAIL_* and PESTRAIL_* keys are read from this file. If TESTRAIL_API_KEY is set in
|
|
7
|
+
# the real environment, TESTRAIL_URL / TESTRAIL_CA_BUNDLE / TESTRAIL_ALLOW_HTTP must be set
|
|
8
|
+
# there too (pestrail refuses to take them from this file).
|
|
9
|
+
TESTRAIL_URL=https://your-company.testrail.io
|
|
10
|
+
TESTRAIL_USERNAME=you@example.com
|
|
11
|
+
TESTRAIL_API_KEY=your-api-key-here
|
|
12
|
+
|
|
13
|
+
# Optional
|
|
14
|
+
# TESTRAIL_READ_ONLY=true
|
|
15
|
+
# Required for write mode (unless TESTRAIL_ALLOW_ANY_ATTACHMENT_PATH=true):
|
|
16
|
+
# Separate multiple directories with ';' on Windows and ':' on macOS/Linux.
|
|
17
|
+
TESTRAIL_ATTACHMENT_ROOTS=C:\Users\you\test-artifacts;D:\ci-logs
|
|
18
|
+
# TESTRAIL_ALLOW_ANY_ATTACHMENT_PATH=true
|
|
19
|
+
# Optional write-scope allowlists (comma-separated; prefixes OK: P1, R56):
|
|
20
|
+
# TESTRAIL_ALLOWED_PROJECT_IDS=1,P3
|
|
21
|
+
# TESTRAIL_ALLOWED_RUN_IDS=R56,78
|
|
22
|
+
# TESTRAIL_TIMEOUT=30
|
|
23
|
+
# TESTRAIL_UPLOAD_TIMEOUT=300
|
|
24
|
+
# Only if your company's CA is not in the OS certificate store:
|
|
25
|
+
# TESTRAIL_CA_BUNDLE=C:\certs\company-root-ca.pem
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.env
|
|
2
|
+
.env.*
|
|
3
|
+
!.env.example
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*.egg-info/
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
htmlcov/
|
|
16
|
+
.vscode/
|
|
17
|
+
.idea/
|
|
18
|
+
.DS_Store
|
|
19
|
+
Thumbs.db
|
|
20
|
+
.claude/
|
|
21
|
+
.flake8
|
|
22
|
+
# Local audit progress tracker (not committed)
|
|
23
|
+
AUDIT_TODO.md
|
pestrail-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pestrail contributors
|
|
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.
|
pestrail-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pestrail
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A safety-first MCP server for TestRail: browse projects, cases, runs and plans, add results and attachments. It cannot delete anything.
|
|
5
|
+
Project-URL: Homepage, https://github.com/poyrazemun/pestrail
|
|
6
|
+
Project-URL: Issues, https://github.com/poyrazemun/pestrail/issues
|
|
7
|
+
Author: pestrail contributors
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: claude,mcp,model-context-protocol,qa,test-management,testrail
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: httpx<0.29,>=0.28
|
|
23
|
+
Requires-Dist: mcp<3,>=2.3
|
|
24
|
+
Requires-Dist: pydantic<3,>=2.14
|
|
25
|
+
Requires-Dist: python-dotenv<2,>=1.0
|
|
26
|
+
Requires-Dist: truststore<1,>=0.10
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# pestrail
|
|
30
|
+
|
|
31
|
+
**A safety-first [MCP](https://modelcontextprotocol.io) server for TestRail.** Let Claude
|
|
32
|
+
Desktop, Claude Code or any MCP client do everyday QA work in TestRail: browse projects,
|
|
33
|
+
cases, runs and plans, read tests and results, **add results** and **attach files** (logs,
|
|
34
|
+
screenshots, reports).
|
|
35
|
+
|
|
36
|
+
**pestrail cannot delete anything**, cannot create, edit or move test cases, suites or
|
|
37
|
+
sections, and cannot close runs or plans. These operations are missing by design and
|
|
38
|
+
blocked in code, not just in prompts. Everything it writes is additive: new results and new
|
|
39
|
+
attachments. See [docs/safety.md](https://github.com/poyrazemun/pestrail/blob/main/docs/safety.md).
|
|
40
|
+
|
|
41
|
+
> Community project, **not affiliated with or endorsed by TestRail, Gurock Software or
|
|
42
|
+
> Idera, Inc.** "TestRail" is a trademark of its respective owner.
|
|
43
|
+
|
|
44
|
+
## Tools
|
|
45
|
+
|
|
46
|
+
- **Read (29):** projects, suites, sections, cases, case/result fields, statuses, priorities,
|
|
47
|
+
configs, milestones, plans, runs, tests, results, attachment lists, users.
|
|
48
|
+
- **Write (9, hidden in read-only mode):** `add_result`, `add_result_for_case`,
|
|
49
|
+
`add_results_for_cases` (report a whole test session in one call),
|
|
50
|
+
`add_result_with_attachments`, and `add_attachment_to_result` / `_run` / `_case` /
|
|
51
|
+
`_plan` / `_plan_entry`.
|
|
52
|
+
|
|
53
|
+
IDs can be plain numbers or TestRail-style (`C1234`, `R56`, `T678`), and statuses can be
|
|
54
|
+
given by name (`"Failed"`, custom statuses too).
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
1. **Get an API key.** A TestRail admin enables the API once (**Administration > Site
|
|
59
|
+
Settings > API**). Then go to **My Settings > API Keys > Add Key**. Prefer a dedicated
|
|
60
|
+
user with a role that can only view and add results.
|
|
61
|
+
2. **Install [uv](https://docs.astral.sh/uv/getting-started/installation/).** pestrail isn't
|
|
62
|
+
on PyPI yet, so it runs from GitHub with `uvx --from git+https://github.com/poyrazemun/pestrail pestrail`.
|
|
63
|
+
3. **Add it to your MCP client.** Start read-only and enable writes later.
|
|
64
|
+
|
|
65
|
+
**Claude Desktop:** open **Settings > Desktop app > Extensions > Manage > Edit Config**, add the block below, then
|
|
66
|
+
quit Claude Desktop completely and start it again.
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"testrail": {
|
|
72
|
+
"command": "uvx",
|
|
73
|
+
"args": ["--from", "git+https://github.com/poyrazemun/pestrail", "pestrail"],
|
|
74
|
+
"env": {
|
|
75
|
+
"TESTRAIL_URL": "https://your-company.testrail.io",
|
|
76
|
+
"TESTRAIL_USERNAME": "you@your-company.com",
|
|
77
|
+
"TESTRAIL_API_KEY": "your-api-key",
|
|
78
|
+
"TESTRAIL_READ_ONLY": "true",
|
|
79
|
+
"PESTRAIL_ENV_FILE": "none"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Claude Code:**
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
claude mcp add testrail --scope user \
|
|
90
|
+
-e TESTRAIL_URL=https://your-company.testrail.io \
|
|
91
|
+
-e TESTRAIL_USERNAME=you@your-company.com \
|
|
92
|
+
-e TESTRAIL_API_KEY=your-api-key \
|
|
93
|
+
-e TESTRAIL_READ_ONLY=true \
|
|
94
|
+
-e PESTRAIL_ENV_FILE=none \
|
|
95
|
+
-- uvx --from git+https://github.com/poyrazemun/pestrail pestrail
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
(In PowerShell, end lines with a backtick `` ` `` instead of `\`.)
|
|
99
|
+
|
|
100
|
+
**Enable writes:** remove `TESTRAIL_READ_ONLY` and set `TESTRAIL_ATTACHMENT_ROOTS` to the
|
|
101
|
+
folders holding your test artifacts (`;`-separated on Windows, `:` on macOS/Linux). Write
|
|
102
|
+
mode won't start without it. Optionally limit writes with `TESTRAIL_ALLOWED_PROJECT_IDS` /
|
|
103
|
+
`TESTRAIL_ALLOWED_RUN_IDS`.
|
|
104
|
+
|
|
105
|
+
## Key settings
|
|
106
|
+
|
|
107
|
+
| Variable | Purpose |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `TESTRAIL_URL`, `TESTRAIL_USERNAME`, `TESTRAIL_API_KEY` | Required. `https://` only. |
|
|
110
|
+
| `TESTRAIL_READ_ONLY` | `true` registers only the read tools. |
|
|
111
|
+
| `TESTRAIL_ATTACHMENT_ROOTS` | Folders attachments may be uploaded from. Required in write mode. |
|
|
112
|
+
| `TESTRAIL_ALLOWED_PROJECT_IDS` / `TESTRAIL_ALLOWED_RUN_IDS` | Optional: confine writes to these projects/runs. |
|
|
113
|
+
|
|
114
|
+
The full reference covers timeouts, `.env` files, proxies and corporate TLS:
|
|
115
|
+
[docs/configuration.md](https://github.com/poyrazemun/pestrail/blob/main/docs/configuration.md).
|
|
116
|
+
|
|
117
|
+
## Example prompts
|
|
118
|
+
|
|
119
|
+
- *"In run R56, which tests are assigned to me? My TestRail email is me@your-company.com."*
|
|
120
|
+
- *"Show me all Failed or Retest tests in run R56 with their latest comment."*
|
|
121
|
+
- *"Report my session to run R56: C101 passed, C102 failed (defect UI-55), C103 blocked."*
|
|
122
|
+
- *"Add a Failed result to C1234 in R56 and attach `C:\artifacts\login.log`."*
|
|
123
|
+
|
|
124
|
+
Tip: ask the assistant to show you what it will add and wait for your OK. pestrail can't
|
|
125
|
+
remove a result once it's added.
|
|
126
|
+
|
|
127
|
+
## More
|
|
128
|
+
|
|
129
|
+
- [Configuration](https://github.com/poyrazemun/pestrail/blob/main/docs/configuration.md): all settings, `.env` files, proxies/TLS, more
|
|
130
|
+
client examples
|
|
131
|
+
- [Safety model and limits](https://github.com/poyrazemun/pestrail/blob/main/docs/safety.md)
|
|
132
|
+
- [Troubleshooting](https://github.com/poyrazemun/pestrail/blob/main/docs/troubleshooting.md)
|
|
133
|
+
- [Contributing](https://github.com/poyrazemun/pestrail/blob/main/CONTRIBUTING.md) · [Security policy](https://github.com/poyrazemun/pestrail/blob/main/SECURITY.md)
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
[MIT](https://github.com/poyrazemun/pestrail/blob/main/LICENSE)
|
pestrail-0.1.0/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# pestrail
|
|
2
|
+
|
|
3
|
+
**A safety-first [MCP](https://modelcontextprotocol.io) server for TestRail.** Let Claude
|
|
4
|
+
Desktop, Claude Code or any MCP client do everyday QA work in TestRail: browse projects,
|
|
5
|
+
cases, runs and plans, read tests and results, **add results** and **attach files** (logs,
|
|
6
|
+
screenshots, reports).
|
|
7
|
+
|
|
8
|
+
**pestrail cannot delete anything**, cannot create, edit or move test cases, suites or
|
|
9
|
+
sections, and cannot close runs or plans. These operations are missing by design and
|
|
10
|
+
blocked in code, not just in prompts. Everything it writes is additive: new results and new
|
|
11
|
+
attachments. See [docs/safety.md](https://github.com/poyrazemun/pestrail/blob/main/docs/safety.md).
|
|
12
|
+
|
|
13
|
+
> Community project, **not affiliated with or endorsed by TestRail, Gurock Software or
|
|
14
|
+
> Idera, Inc.** "TestRail" is a trademark of its respective owner.
|
|
15
|
+
|
|
16
|
+
## Tools
|
|
17
|
+
|
|
18
|
+
- **Read (29):** projects, suites, sections, cases, case/result fields, statuses, priorities,
|
|
19
|
+
configs, milestones, plans, runs, tests, results, attachment lists, users.
|
|
20
|
+
- **Write (9, hidden in read-only mode):** `add_result`, `add_result_for_case`,
|
|
21
|
+
`add_results_for_cases` (report a whole test session in one call),
|
|
22
|
+
`add_result_with_attachments`, and `add_attachment_to_result` / `_run` / `_case` /
|
|
23
|
+
`_plan` / `_plan_entry`.
|
|
24
|
+
|
|
25
|
+
IDs can be plain numbers or TestRail-style (`C1234`, `R56`, `T678`), and statuses can be
|
|
26
|
+
given by name (`"Failed"`, custom statuses too).
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
1. **Get an API key.** A TestRail admin enables the API once (**Administration > Site
|
|
31
|
+
Settings > API**). Then go to **My Settings > API Keys > Add Key**. Prefer a dedicated
|
|
32
|
+
user with a role that can only view and add results.
|
|
33
|
+
2. **Install [uv](https://docs.astral.sh/uv/getting-started/installation/).** pestrail isn't
|
|
34
|
+
on PyPI yet, so it runs from GitHub with `uvx --from git+https://github.com/poyrazemun/pestrail pestrail`.
|
|
35
|
+
3. **Add it to your MCP client.** Start read-only and enable writes later.
|
|
36
|
+
|
|
37
|
+
**Claude Desktop:** open **Settings > Desktop app > Extensions > Manage > Edit Config**, add the block below, then
|
|
38
|
+
quit Claude Desktop completely and start it again.
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"testrail": {
|
|
44
|
+
"command": "uvx",
|
|
45
|
+
"args": ["--from", "git+https://github.com/poyrazemun/pestrail", "pestrail"],
|
|
46
|
+
"env": {
|
|
47
|
+
"TESTRAIL_URL": "https://your-company.testrail.io",
|
|
48
|
+
"TESTRAIL_USERNAME": "you@your-company.com",
|
|
49
|
+
"TESTRAIL_API_KEY": "your-api-key",
|
|
50
|
+
"TESTRAIL_READ_ONLY": "true",
|
|
51
|
+
"PESTRAIL_ENV_FILE": "none"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Claude Code:**
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
claude mcp add testrail --scope user \
|
|
62
|
+
-e TESTRAIL_URL=https://your-company.testrail.io \
|
|
63
|
+
-e TESTRAIL_USERNAME=you@your-company.com \
|
|
64
|
+
-e TESTRAIL_API_KEY=your-api-key \
|
|
65
|
+
-e TESTRAIL_READ_ONLY=true \
|
|
66
|
+
-e PESTRAIL_ENV_FILE=none \
|
|
67
|
+
-- uvx --from git+https://github.com/poyrazemun/pestrail pestrail
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
(In PowerShell, end lines with a backtick `` ` `` instead of `\`.)
|
|
71
|
+
|
|
72
|
+
**Enable writes:** remove `TESTRAIL_READ_ONLY` and set `TESTRAIL_ATTACHMENT_ROOTS` to the
|
|
73
|
+
folders holding your test artifacts (`;`-separated on Windows, `:` on macOS/Linux). Write
|
|
74
|
+
mode won't start without it. Optionally limit writes with `TESTRAIL_ALLOWED_PROJECT_IDS` /
|
|
75
|
+
`TESTRAIL_ALLOWED_RUN_IDS`.
|
|
76
|
+
|
|
77
|
+
## Key settings
|
|
78
|
+
|
|
79
|
+
| Variable | Purpose |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `TESTRAIL_URL`, `TESTRAIL_USERNAME`, `TESTRAIL_API_KEY` | Required. `https://` only. |
|
|
82
|
+
| `TESTRAIL_READ_ONLY` | `true` registers only the read tools. |
|
|
83
|
+
| `TESTRAIL_ATTACHMENT_ROOTS` | Folders attachments may be uploaded from. Required in write mode. |
|
|
84
|
+
| `TESTRAIL_ALLOWED_PROJECT_IDS` / `TESTRAIL_ALLOWED_RUN_IDS` | Optional: confine writes to these projects/runs. |
|
|
85
|
+
|
|
86
|
+
The full reference covers timeouts, `.env` files, proxies and corporate TLS:
|
|
87
|
+
[docs/configuration.md](https://github.com/poyrazemun/pestrail/blob/main/docs/configuration.md).
|
|
88
|
+
|
|
89
|
+
## Example prompts
|
|
90
|
+
|
|
91
|
+
- *"In run R56, which tests are assigned to me? My TestRail email is me@your-company.com."*
|
|
92
|
+
- *"Show me all Failed or Retest tests in run R56 with their latest comment."*
|
|
93
|
+
- *"Report my session to run R56: C101 passed, C102 failed (defect UI-55), C103 blocked."*
|
|
94
|
+
- *"Add a Failed result to C1234 in R56 and attach `C:\artifacts\login.log`."*
|
|
95
|
+
|
|
96
|
+
Tip: ask the assistant to show you what it will add and wait for your OK. pestrail can't
|
|
97
|
+
remove a result once it's added.
|
|
98
|
+
|
|
99
|
+
## More
|
|
100
|
+
|
|
101
|
+
- [Configuration](https://github.com/poyrazemun/pestrail/blob/main/docs/configuration.md): all settings, `.env` files, proxies/TLS, more
|
|
102
|
+
client examples
|
|
103
|
+
- [Safety model and limits](https://github.com/poyrazemun/pestrail/blob/main/docs/safety.md)
|
|
104
|
+
- [Troubleshooting](https://github.com/poyrazemun/pestrail/blob/main/docs/troubleshooting.md)
|
|
105
|
+
- [Contributing](https://github.com/poyrazemun/pestrail/blob/main/CONTRIBUTING.md) · [Security policy](https://github.com/poyrazemun/pestrail/blob/main/SECURITY.md)
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
[MIT](https://github.com/poyrazemun/pestrail/blob/main/LICENSE)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pestrail"
|
|
3
|
+
# Single source of truth: __version__ in src/pestrail/__init__.py (see [tool.hatch.version]).
|
|
4
|
+
dynamic = ["version"]
|
|
5
|
+
description = "A safety-first MCP server for TestRail: browse projects, cases, runs and plans, add results and attachments. It cannot delete anything."
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
authors = [{ name = "pestrail contributors" }]
|
|
11
|
+
keywords = ["testrail", "mcp", "model-context-protocol", "qa", "test-management", "claude"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
22
|
+
"Topic :: Software Development :: Testing",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"mcp>=2.3,<3",
|
|
26
|
+
"httpx>=0.28,<0.29",
|
|
27
|
+
"pydantic>=2.14,<3",
|
|
28
|
+
"python-dotenv>=1.0,<2",
|
|
29
|
+
"truststore>=0.10,<1",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
pestrail = "pestrail.__main__:main"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/poyrazemun/pestrail"
|
|
37
|
+
Issues = "https://github.com/poyrazemun/pestrail/issues"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=8",
|
|
42
|
+
"respx>=0.21",
|
|
43
|
+
"ruff>=0.6",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["hatchling>=1.26"]
|
|
48
|
+
build-backend = "hatchling.build"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.version]
|
|
51
|
+
path = "src/pestrail/__init__.py"
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.wheel]
|
|
54
|
+
packages = ["src/pestrail"]
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.sdist]
|
|
57
|
+
include = ["src/pestrail", "tests", "README.md", "LICENSE", ".env.example", "pyproject.toml"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
addopts = "-ra"
|
|
62
|
+
|
|
63
|
+
[tool.ruff]
|
|
64
|
+
line-length = 100
|
|
65
|
+
target-version = "py311"
|
|
66
|
+
src = ["src", "tests"]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint]
|
|
69
|
+
select = ["E", "F", "W", "I", "B", "UP", "S", "SIM", "RUF"]
|
|
70
|
+
ignore = []
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint.per-file-ignores]
|
|
73
|
+
"tests/**" = ["S101", "S105", "S106", "S108"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Entry point: ``pestrail`` / ``python -m pestrail`` runs the MCP server over stdio.
|
|
2
|
+
|
|
3
|
+
stdout carries MCP protocol messages only; all diagnostics go to stderr.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_dotenv() -> None:
|
|
18
|
+
"""Load the per-user .env file, or the one named by PESTRAIL_ENV_FILE.
|
|
19
|
+
|
|
20
|
+
See config.env_file_location for which file is used (never the working directory's
|
|
21
|
+
.env implicitly) and config.apply_env_file for the merge rules (real env wins, only
|
|
22
|
+
TESTRAIL_*/PESTRAIL_* keys, no destination settings from the file when the API key
|
|
23
|
+
comes from the environment).
|
|
24
|
+
"""
|
|
25
|
+
from .config import ConfigError, apply_env_file, env_file_location
|
|
26
|
+
|
|
27
|
+
path, explicit = env_file_location()
|
|
28
|
+
if path is None:
|
|
29
|
+
return
|
|
30
|
+
if not path.is_file():
|
|
31
|
+
if explicit:
|
|
32
|
+
raise ConfigError(f"PESTRAIL_ENV_FILE not found: {path}")
|
|
33
|
+
return
|
|
34
|
+
from dotenv import dotenv_values
|
|
35
|
+
|
|
36
|
+
applied, ignored = apply_env_file(dotenv_values(path), os.environ)
|
|
37
|
+
log = logging.getLogger("pestrail")
|
|
38
|
+
if applied:
|
|
39
|
+
log.info("Loaded %s from %s", ", ".join(sorted(applied)), path)
|
|
40
|
+
if ignored:
|
|
41
|
+
log.warning("Ignored non-pestrail keys in %s: %s", path, ", ".join(sorted(ignored)))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main(argv: list[str] | None = None) -> None:
|
|
45
|
+
parser = argparse.ArgumentParser(
|
|
46
|
+
prog="pestrail", description="Safety-first MCP server for TestRail (stdio)."
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument("--version", action="version", version=f"pestrail {__version__}")
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--log-level",
|
|
51
|
+
default=os.environ.get("PESTRAIL_LOG_LEVEL", "WARNING"),
|
|
52
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
|
53
|
+
help="Log level for stderr output (default: WARNING).",
|
|
54
|
+
)
|
|
55
|
+
args = parser.parse_args(argv)
|
|
56
|
+
|
|
57
|
+
logging.basicConfig(
|
|
58
|
+
level=args.log_level,
|
|
59
|
+
stream=sys.stderr,
|
|
60
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
61
|
+
)
|
|
62
|
+
# httpx logs every request URL at INFO; keep that out of normal logs.
|
|
63
|
+
logging.getLogger("httpx").setLevel(max(logging.WARNING, logging.getLogger().level))
|
|
64
|
+
|
|
65
|
+
from .config import ConfigError, env_file_location, load_config
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
_load_dotenv()
|
|
69
|
+
config = load_config()
|
|
70
|
+
except ConfigError as exc:
|
|
71
|
+
message = f"pestrail: configuration error: {exc}"
|
|
72
|
+
local_env = Path.cwd() / ".env"
|
|
73
|
+
if "Missing required" in str(exc) and local_env.is_file():
|
|
74
|
+
path, _ = env_file_location()
|
|
75
|
+
message += (
|
|
76
|
+
f"\npestrail: note: {local_env} exists but is not loaded automatically. "
|
|
77
|
+
"Set PESTRAIL_ENV_FILE to its path to use it"
|
|
78
|
+
+ (f", or move it to {path}." if path else ".")
|
|
79
|
+
)
|
|
80
|
+
print(message, file=sys.stderr)
|
|
81
|
+
sys.exit(2)
|
|
82
|
+
|
|
83
|
+
from .server import create_server
|
|
84
|
+
|
|
85
|
+
server = create_server(config)
|
|
86
|
+
server.run("stdio")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
main()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Local file validation for attachment uploads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .client import TestRailError
|
|
9
|
+
|
|
10
|
+
#: TestRail's documented maximum attachment size.
|
|
11
|
+
MAX_ATTACHMENT_BYTES = 256 * 1024 * 1024
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AttachmentError(TestRailError):
|
|
15
|
+
"""Raised when a local file may not be uploaded."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_attachment_path(
|
|
19
|
+
raw_path: str,
|
|
20
|
+
roots: Sequence[Path],
|
|
21
|
+
*,
|
|
22
|
+
max_bytes: int = MAX_ATTACHMENT_BYTES,
|
|
23
|
+
allow_any_path: bool = False,
|
|
24
|
+
) -> Path:
|
|
25
|
+
"""Resolve ``raw_path`` and check it is an existing, allowed, not-too-large regular file.
|
|
26
|
+
|
|
27
|
+
* ``~`` is expanded.
|
|
28
|
+
* Relative paths are resolved against the attachment roots (the first root where the
|
|
29
|
+
file exists wins), never against the server's working directory, which MCP clients
|
|
30
|
+
set unpredictably. Without roots, relative paths are rejected.
|
|
31
|
+
* Symlinks and ``..`` are resolved *before* the root check, so a link inside a root that
|
|
32
|
+
points outside it is rejected.
|
|
33
|
+
* If ``roots`` is empty, uploads are refused unless ``allow_any_path`` is set (explicit
|
|
34
|
+
opt-in to uploading any readable file).
|
|
35
|
+
"""
|
|
36
|
+
if not raw_path or not str(raw_path).strip():
|
|
37
|
+
raise AttachmentError("A file path is required.")
|
|
38
|
+
if "\x00" in raw_path:
|
|
39
|
+
raise AttachmentError("Invalid file path.")
|
|
40
|
+
if not roots and not allow_any_path:
|
|
41
|
+
raise AttachmentError(
|
|
42
|
+
"Uploads require TESTRAIL_ATTACHMENT_ROOTS (or "
|
|
43
|
+
"TESTRAIL_ALLOW_ANY_ATTACHMENT_PATH=true to allow any readable file)."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
candidate = Path(str(raw_path).strip().strip('"')).expanduser()
|
|
47
|
+
if not candidate.is_absolute():
|
|
48
|
+
candidate = _resolve_relative(candidate, raw_path, roots)
|
|
49
|
+
try:
|
|
50
|
+
resolved = candidate.resolve(strict=True)
|
|
51
|
+
except FileNotFoundError as exc:
|
|
52
|
+
raise AttachmentError(f"File not found: {raw_path}") from exc
|
|
53
|
+
except (OSError, RuntimeError) as exc:
|
|
54
|
+
raise AttachmentError(f"Cannot access file {raw_path}: {type(exc).__name__}") from exc
|
|
55
|
+
|
|
56
|
+
if roots and not any(_is_within(resolved, root) for root in roots):
|
|
57
|
+
allowed = ", ".join(str(r) for r in roots)
|
|
58
|
+
raise AttachmentError(
|
|
59
|
+
f"File {raw_path} is outside the allowed attachment directories "
|
|
60
|
+
f"(TESTRAIL_ATTACHMENT_ROOTS): {allowed}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if not resolved.is_file():
|
|
64
|
+
raise AttachmentError(f"Not a regular file: {raw_path}")
|
|
65
|
+
|
|
66
|
+
size = resolved.stat().st_size
|
|
67
|
+
if size > max_bytes:
|
|
68
|
+
raise AttachmentError(
|
|
69
|
+
f"File {resolved.name} is {size / 1024 / 1024:.1f} MB; "
|
|
70
|
+
f"TestRail's limit is {max_bytes // 1024 // 1024} MB."
|
|
71
|
+
)
|
|
72
|
+
return resolved
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _resolve_relative(candidate: Path, raw_path: str, roots: Sequence[Path]) -> Path:
|
|
76
|
+
if not roots:
|
|
77
|
+
raise AttachmentError(
|
|
78
|
+
f"Relative path {raw_path!r} is not supported: use an absolute path "
|
|
79
|
+
"(or set TESTRAIL_ATTACHMENT_ROOTS so relative paths are resolved against it)."
|
|
80
|
+
)
|
|
81
|
+
for root in roots:
|
|
82
|
+
joined = root / candidate
|
|
83
|
+
if joined.exists():
|
|
84
|
+
return joined
|
|
85
|
+
searched = ", ".join(str(r) for r in roots)
|
|
86
|
+
raise AttachmentError(f"File not found: {raw_path} (searched in: {searched})")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _is_within(path: Path, root: Path) -> bool:
|
|
90
|
+
try:
|
|
91
|
+
root_resolved = root.resolve(strict=True)
|
|
92
|
+
except (OSError, RuntimeError):
|
|
93
|
+
return False
|
|
94
|
+
# is_relative_to compares whole path parts, so "/data/logs2" is not inside "/data/logs".
|
|
95
|
+
# WindowsPath comparisons are case-insensitive, matching NTFS semantics.
|
|
96
|
+
return path.is_relative_to(root_resolved)
|