testmo_upload_case_attachment
Upload a file attachment to a test case, automatically compressing large images. Requires an absolute path to a saved file.
Instructions
Upload a single file attachment to a test case. Large images are auto-compressed.
IMPORTANT: file_path must be an absolute path to a file saved on disk (e.g. /Users/jan/Desktop/screenshot.png). Pasted images or image data from the conversation cannot be uploaded — the user must save the file first and provide its path. If no path is provided or the user has not saved the file yet, ask them to save it and share the full file path.
Args: case_id: The test case ID. file_path: Absolute path to the local file to upload (e.g. /Users/jan/Desktop/screenshot.png).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| case_id | Yes | ||
| file_path | Yes |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- testmo/tools/attachments.py:62-83 (handler)The async function `testmo_upload_case_attachment` is the actual tool handler. It takes a `case_id` (int) and `file_path` (str), validates file_path, prepares the file via `_prepare_file` (compressing large images), and uploads it via `_upload` to the Testmo API endpoint `/cases/{case_id}/attachments/single`.
@mcp.tool() async def testmo_upload_case_attachment( case_id: int, file_path: str, ) -> dict[str, Any]: """Upload a single file attachment to a test case. Large images are auto-compressed. IMPORTANT: file_path must be an absolute path to a file saved on disk (e.g. /Users/jan/Desktop/screenshot.png). Pasted images or image data from the conversation cannot be uploaded — the user must save the file first and provide its path. If no path is provided or the user has not saved the file yet, ask them to save it and share the full file path. Args: case_id: The test case ID. file_path: Absolute path to the local file to upload (e.g. /Users/jan/Desktop/screenshot.png). """ if not file_path or not file_path.strip(): raise ValueError("file_path is required. Ask the user to save the file to disk and provide the full path (e.g. /Users/jan/Desktop/screenshot.png).") filename, file_content, content_type = _prepare_file(file_path) return await _upload( f"/cases/{case_id}/attachments/single", [("file", (filename, file_content, content_type))], ) - testmo/tools/attachments.py:8-8 (registration)The `mcp` instance is imported from `..server`, which is a `FastMCP("testmo-mcp")` instance. The `@mcp.tool()` decorator on line 62 registers the tool.
from ..server import mcp - testmo/tools/attachments.py:15-38 (helper)The `_prepare_file` helper function reads a file from disk, compresses large images (>1MB) by converting to JPEG with adaptive quality, and returns the filename, bytes, and MIME content type.
def _prepare_file(file_path: str) -> tuple[str, bytes, str]: """Read a file and compress it if it's a large image. Returns (filename, content, content_type).""" path = Path(file_path) if not path.exists(): raise ValueError(f"File not found: {file_path}") file_content = path.read_bytes() suffix = path.suffix.lower() if suffix in IMAGE_EXTENSIONS and len(file_content) > MAX_IMAGE_SIZE: img = Image.open(io.BytesIO(file_content)) img = img.convert("RGB") buf = io.BytesIO() quality = 85 img.save(buf, format="JPEG", quality=quality, optimize=True) while buf.tell() > MAX_IMAGE_SIZE and quality > 20: quality -= 10 buf = io.BytesIO() img.save(buf, format="JPEG", quality=quality, optimize=True) file_content = buf.getvalue() filename = path.stem + ".jpg" content_type = "image/jpeg" else: filename = path.name content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream" return filename, file_content, content_type - testmo/client.py:52-77 (helper)The `_upload` helper function performs the multipart form upload via httpx to the Testmo API, handling authentication and error responses. Used by the tool to send the file.
async def _upload( endpoint: str, files: list[tuple[str, tuple[str, bytes, str]]], ) -> dict[str, Any]: """Upload one or more files via multipart form.""" if not TESTMO_URL or not TESTMO_API_KEY: raise ValueError("TESTMO_URL and TESTMO_API_KEY must be set") async with httpx.AsyncClient( base_url=f"{TESTMO_URL}/api/v1/", headers={ "Authorization": f"Bearer {TESTMO_API_KEY}", "Accept": "application/json", }, timeout=httpx.Timeout(UPLOAD_TIMEOUT), ) as client: response = await client.post(endpoint, files=files) if response.status_code == 204: return {"success": True} if response.status_code >= 400: try: error_body = response.json() except Exception: error_body = response.text raise RuntimeError(f"Upload failed {response.status_code}: {error_body}") result = response.json() return result.get("result", result) - testmo-mcp.py:16-16 (registration)The `testmo.tools.attachments` module is imported in the main entry point, which causes the `@mcp.tool()` decorators in that module to execute and register all attachment tools including `testmo_upload_case_attachment`.
import testmo.tools.attachments # noqa: F401