Add: download_decisive_appraisal_pdf MCP tool
The PDF host (free-justice.openapi.gov.il) requires the same x-client-id header as the search API, so a normal browser click on the URL fails. This tool carries the auth header automatically and saves the PDF to a configurable local directory. Safety properties: - SSRF guard: only download from free-justice.openapi.gov.il and pub-justice.openapi.gov.il. - Path-traversal guard: filename is reduced to its basename; arbitrary paths are stripped. - Content-type guard: rejects 200-OK responses whose body is not a real PDF (gateway sometimes returns JSON-error 200s). - Atomic write via .tmp + rename so partial downloads never replace the cached copy. - Caches by destination path; re-downloads are no-ops unless overwrite=True. DECISIVE_APPRAISER_DOWNLOAD_DIR env var configures the output dir (default: ./downloads/decisive_appraisals). 6 new unit tests cover the happy path, caching, and all four guards. End-to-end live test confirmed a 1.4MB real PDF lands on disk with valid `%PDF-1.7` header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1420,6 +1420,55 @@ def search_decisive_appraisals(
|
||||
return f"Error searching decisive appraisals: {str(e)}"
|
||||
|
||||
|
||||
@conditional_tool("tool_download_decisive_appraisal_pdf_enabled")
|
||||
def download_decisive_appraisal_pdf(
|
||||
url: str,
|
||||
filename: Optional[str] = None,
|
||||
output_dir: Optional[str] = None,
|
||||
overwrite: bool = False,
|
||||
) -> str:
|
||||
"""Download the PDF of a decisive-appraiser (שמאי מכריע) decision.
|
||||
|
||||
The PDF host requires the same `x-client-id` header as the search API,
|
||||
so a normal browser click on the URL will fail. This tool carries the
|
||||
auth header automatically.
|
||||
|
||||
Args:
|
||||
url: Direct PDF URL, as returned in `decisions[].pdf_url` from
|
||||
search_decisive_appraisals. Must be on free-justice.openapi.gov.il.
|
||||
filename: Optional filename for the saved PDF. Path components are
|
||||
stripped; only the basename is used. If omitted, a hash-based
|
||||
filename is generated from the URL.
|
||||
output_dir: Optional override for the output directory. Defaults to
|
||||
the configured `DECISIVE_APPRAISER_DOWNLOAD_DIR`
|
||||
(./downloads/decisive_appraisals).
|
||||
overwrite: If False (default), returns the cached file when it
|
||||
already exists at the destination.
|
||||
|
||||
Returns:
|
||||
JSON string with `path` (absolute local file path), `size_bytes`,
|
||||
`from_cache` (true if file already existed), `content_type`.
|
||||
"""
|
||||
log_mcp_call(
|
||||
"download_decisive_appraisal_pdf",
|
||||
url=url,
|
||||
filename=filename,
|
||||
output_dir=output_dir,
|
||||
overwrite=overwrite,
|
||||
)
|
||||
try:
|
||||
result = decisive_appraiser_client.download_pdf(
|
||||
url=url,
|
||||
output_dir=output_dir,
|
||||
filename=filename,
|
||||
overwrite=overwrite,
|
||||
)
|
||||
return json.dumps(result, ensure_ascii=False, indent=None)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in download_decisive_appraisal_pdf: {e}", exc_info=True)
|
||||
return f"Error downloading appraisal PDF: {str(e)}"
|
||||
|
||||
|
||||
# Health check endpoint for HTTP deployments
|
||||
@mcp.custom_route("/health", methods=["GET"])
|
||||
async def health_check(request):
|
||||
|
||||
Reference in New Issue
Block a user