testmo_upload_case_attachments
Upload multiple file attachments to a test case in one request. Large images are auto-compressed.
Instructions
Upload up to 20 file attachments to a test case in one request. Large images are auto-compressed.
IMPORTANT: Each path must be an absolute path to a file saved on disk. Pasted images or image data from the conversation cannot be uploaded — the user must save the files first. If no paths are provided, ask the user to save the files and share their full paths.
Args: case_id: The test case ID. file_paths: List of absolute paths to local files to upload (max 20).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| case_id | Yes | ||
| file_paths | Yes |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- testmo/tools/attachments.py:87-109 (handler)The main handler function for the 'testmo_upload_case_attachments' tool. Accepts case_id and file_paths list (max 20), prepares each file via _prepare_file, and uploads them via _upload to /cases/{case_id}/attachments.
async def testmo_upload_case_attachments( case_id: int, file_paths: list[str], ) -> dict[str, Any]: """Upload up to 20 file attachments to a test case in one request. Large images are auto-compressed. IMPORTANT: Each path must be an absolute path to a file saved on disk. Pasted images or image data from the conversation cannot be uploaded — the user must save the files first. If no paths are provided, ask the user to save the files and share their full paths. Args: case_id: The test case ID. file_paths: List of absolute paths to local files to upload (max 20). """ if not file_paths: raise ValueError("file_paths must not be empty") if len(file_paths) > 20: file_paths = file_paths[:20] files = [] for fp in file_paths: filename, file_content, content_type = _prepare_file(fp) files.append(("file", (filename, file_content, content_type))) return await _upload(f"/cases/{case_id}/attachments", files) - testmo/tools/attachments.py:86-87 (registration)The tool is registered as an MCP tool via the @mcp.tool() decorator on the async function. The 'mcp' instance is imported from ..server (testmo/server.py). The module is imported in testmo-mcp.py line 16.
@mcp.tool() async def testmo_upload_case_attachments( - testmo/tools/attachments.py:15-38 (helper)Helper function _prepare_file reads a file from disk, auto-compresses large images (>1MB) to JPEG, and returns (filename, content, content_type). Used by the upload handler to process each file path.
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 that performs the actual HTTP multipart POST upload to the Testmo API. Used by the handler to send files.
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)