Files
yargi-mcp/uyusmazlik_mcp_module/client.py
T
saidsurucuandClaude Opus 4.8 fadc3b0bc0 fix(aym,uyusmazlik): adapt to rebuilt AYM and Uyuşmazlık sites
Both sites were rebuilt and their old endpoints now 404:
- AYM moved to a single-page app backed by a JSON API
  (POST /api/core/public/search, kararTipi NormDenetimi/BireyselBasvuru;
  full text via {id, size:1} -> "icerik" HTML). Old /Ara, /ND/, /BB/ gone.
- Uyuşmazlık moved to ASP.NET WebForms (viewstate postback to /, GridView
  results, decisions served as /Uploads/{EsasNo}.pdf). Old /Arama/Search gone.

Changes:
- New anayasa_mcp_module/api_client.py: shared KBB JSON client, base64url
  document-id codec, HTML->Markdown + HTML text stripping helpers.
- Rewrite anayasa client/bireysel_client/unified_client over the new API,
  preserving public method names and response models.
- Rewrite uyusmazlik client for the postback flow + GridView parse + PDF
  document conversion; simplify request model to text + scope + paging.
- Trim search_anayasa_unified and search_uyusmazlik_decisions tool signatures
  to parameters the new APIs actually support; drop dead enums.

Verified live via FastMCP client: search + document retrieval work for AYM
norm/bireysel and Uyuşmazlık.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 19:22:49 +03:00

180 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# uyusmazlik_mcp_module/client.py
#
# Client for the rebuilt Uyuşmazlık Mahkemesi search site
# (https://kararlar.uyusmazlik.gov.tr). The site is an ASP.NET WebForms app:
# searching is a form postback against "/" that returns an HTML page with a
# GridView of results, and each decision is a PDF served from /Uploads/.
#
# The previous AJAX endpoint (/Arama/Search) was retired and now returns 404.
import asyncio
import io
import logging
import re
from typing import Dict, List, Optional
from urllib.parse import urljoin
import httpx
from bs4 import BeautifulSoup
from markitdown import MarkItDown
from .models import (
UyusmazlikSearchRequest,
UyusmazlikApiDecisionEntry,
UyusmazlikSearchResponse,
UyusmazlikDocumentMarkdown,
)
logger = logging.getLogger(__name__)
if not logger.hasHandlers():
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# ASP.NET hidden fields that must be round-tripped on every postback.
_HIDDEN_FIELDS = ("__VIEWSTATE", "__VIEWSTATEGENERATOR", "__EVENTVALIDATION")
class UyusmazlikApiClient:
BASE_URL = "https://kararlar.uyusmazlik.gov.tr"
SEARCH_PATH = "/"
def __init__(self, request_timeout: float = 30.0):
self.request_timeout = request_timeout
# A persistent cookie-aware client so ASP.NET session/viewstate are kept.
self.http_client = httpx.AsyncClient(
base_url=self.BASE_URL,
headers={
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Origin": self.BASE_URL,
"Referer": self.BASE_URL + "/",
},
timeout=request_timeout,
verify=False,
follow_redirects=True,
)
@staticmethod
def _extract_hidden_fields(html_content: str) -> Dict[str, str]:
soup = BeautifulSoup(html_content, "html.parser")
fields: Dict[str, str] = {}
for name in _HIDDEN_FIELDS:
tag = soup.find("input", attrs={"name": name})
fields[name] = tag["value"] if tag and tag.has_attr("value") else ""
return fields
@staticmethod
def _parse_results(html_content: str, base_url: str) -> UyusmazlikSearchResponse:
soup = BeautifulSoup(html_content, "html.parser")
decisions: List[UyusmazlikApiDecisionEntry] = []
grid = soup.find("table", id="GridView1")
if grid:
rows = grid.find_all("tr")
for row in rows[1:]: # skip header row
cells = row.find_all("td")
if len(cells) < 4:
continue
# The İşlemler cell holds the PDF "Görüntüle" link. Pager rows also
# contain <a> tags (javascript:__doPostBack ...), so require a real
# document link and skip everything else.
link_tag = cells[3].find(
"a", href=lambda h: h and not h.strip().lower().startswith("javascript:")
)
if not link_tag:
continue
href = link_tag["href"].strip()
if "uploads" not in href.lower() and not href.lower().endswith(".pdf"):
continue
document_url = urljoin(base_url + "/", href)
decisions.append(UyusmazlikApiDecisionEntry(
esas_sayisi=cells[0].get_text(strip=True) or None,
karar_sayisi=cells[1].get_text(strip=True) or None,
karar_tarihi=cells[2].get_text(strip=True) or None,
document_url=document_url,
))
# Try to read a "N kayıt/sonuç/karar bulundu" style count if present.
total_records: Optional[int] = None
count_match = re.search(r'(\d+)\s*(?:adet\s*)?(?:kayıt|sonuç|karar)\b', html_content, re.IGNORECASE)
if count_match:
total_records = int(count_match.group(1))
return UyusmazlikSearchResponse(decisions=decisions, total_records_found=total_records)
async def search_decisions(self, params: UyusmazlikSearchRequest) -> UyusmazlikSearchResponse:
# 1. Load the landing page to obtain a fresh viewstate + session cookie.
landing = await self.http_client.get(self.SEARCH_PATH)
landing.raise_for_status()
form_data = self._extract_hidden_fields(landing.text)
# 2. Submit the search form.
form_data.update({
"txtSearch": params.icerik or "",
"rblSearchScope": params.search_scope,
"btnSearch": "Ara",
})
if params.case_sensitive:
form_data["chkCaseSensitive"] = "on"
logger.info("UyusmazlikApiClient: search icerik=%r scope=%s page=%s",
params.icerik, params.search_scope, params.page_number)
response = await self.http_client.post(
self.SEARCH_PATH,
data=form_data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
response.raise_for_status()
html_content = response.text
# 3. Navigate the GridView pager if a later page is requested.
if params.page_number > 1:
page_fields = self._extract_hidden_fields(html_content)
page_fields.update({
"txtSearch": params.icerik or "",
"rblSearchScope": params.search_scope,
"__EVENTTARGET": "GridView1",
"__EVENTARGUMENT": f"Page${params.page_number}",
})
if params.case_sensitive:
page_fields["chkCaseSensitive"] = "on"
page_response = await self.http_client.post(
self.SEARCH_PATH,
data=page_fields,
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
page_response.raise_for_status()
html_content = page_response.text
return self._parse_results(html_content, self.BASE_URL)
def _convert_pdf_to_markdown(self, pdf_bytes: bytes) -> Optional[str]:
try:
pdf_stream = io.BytesIO(pdf_bytes)
conversion_result = MarkItDown().convert(pdf_stream, file_extension=".pdf")
return conversion_result.text_content
except Exception as e:
logger.error("UyusmazlikApiClient: PDF to Markdown conversion error: %s", e)
return None
async def get_decision_document_as_markdown(self, document_url: str) -> UyusmazlikDocumentMarkdown:
"""Fetch an Uyuşmazlık decision PDF and return its content as Markdown."""
logger.info("UyusmazlikApiClient: Fetching document PDF from %s", document_url)
try:
response = await self.http_client.get(
document_url,
headers={"Accept": "application/pdf,*/*"},
)
response.raise_for_status()
markdown_content = await asyncio.to_thread(self._convert_pdf_to_markdown, response.content)
return UyusmazlikDocumentMarkdown(source_url=document_url, markdown_content=markdown_content)
except httpx.HTTPError as e:
logger.error("UyusmazlikApiClient: HTTP error fetching document from %s: %s", document_url, e)
raise
async def close_client_session(self):
if hasattr(self, "http_client") and self.http_client and not self.http_client.is_closed:
await self.http_client.aclose()
logger.info("UyusmazlikApiClient: HTTP client session closed.")