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.
@@ -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.
@@ -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)
@@ -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,3 @@
1
+ """pestrail: a safety-first MCP server for TestRail."""
2
+
3
+ __version__ = "0.1.0"
@@ -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)