Merge pull request #3 from saidsurucu/kik

add kik module
This commit is contained in:
saidsurucu
2025-05-27 22:16:41 +03:00
committed by GitHub
6 changed files with 680 additions and 80 deletions
+2
View File
@@ -161,3 +161,5 @@ cython_debug/
.DS_Store .DS_Store
hello.py hello.py
*.html
test_kik_client.py
View File
+441
View File
@@ -0,0 +1,441 @@
# kik_mcp_module/client.py
import asyncio
from playwright.async_api import (
async_playwright,
Page,
BrowserContext,
Browser,
Error as PlaywrightError,
TimeoutError as PlaywrightTimeoutError
)
from bs4 import BeautifulSoup
import logging
from typing import Dict, Any, List, Optional
import urllib.parse
import base64 # Base64 için
import re
import html as html_parser
from markitdown import MarkItDown
import os
import math
import tempfile
from .models import (
KikSearchRequest,
KikDecisionEntry,
KikSearchResult,
KikDocumentMarkdown,
KikKararTipi
)
logger = logging.getLogger(__name__)
class KikApiClient:
BASE_URL = "https://ekap.kik.gov.tr"
SEARCH_PAGE_PATH = "/EKAP/Vatandas/kurulkararsorgu.aspx"
FIELD_LOCATORS = {
"karar_tipi_radio_group": "input[name='ctl00$ContentPlaceHolder1$kurulKararTip']",
"karar_no": "input[name='ctl00$ContentPlaceHolder1$txtKararNo']",
"karar_tarihi_baslangic": "input[name='ctl00$ContentPlaceHolder1$etKararTarihBaslangic$EkapTakvimTextBox_etKararTarihBaslangic']",
"karar_tarihi_bitis": "input[name='ctl00$ContentPlaceHolder1$etKararTarihBitis$EkapTakvimTextBox_etKararTarihBitis']",
"resmi_gazete_sayisi": "input[name='ctl00$ContentPlaceHolder1$txtResmiGazeteSayisi']",
"resmi_gazete_tarihi": "input[name='ctl00$ContentPlaceHolder1$etResmiGazeteTarihi$EkapTakvimTextBox_etResmiGazeteTarihi']",
"basvuru_konusu_ihale": "input[name='ctl00$ContentPlaceHolder1$txtBasvuruKonusuIhale']",
"basvuru_sahibi": "input[name='ctl00$ContentPlaceHolder1$txtSikayetci']",
"ihaleyi_yapan_idare": "input[name='ctl00$ContentPlaceHolder1$txtIhaleyiYapanIdare']",
"yil": "select[name='ctl00$ContentPlaceHolder1$ddlYil']",
"karar_metni": "input[name='ctl00$ContentPlaceHolder1$txtKararMetni']",
"search_button_id": "ctl00_ContentPlaceHolder1_btnAra"
}
RESULTS_TABLE_ID = "grdKurulKararSorguSonuc"
NO_RESULTS_MESSAGE_SELECTOR = "div#ctl00_MessageContent1"
VALIDATION_SUMMARY_SELECTOR = "div#ctl00_ValidationSummary1"
MODAL_CLOSE_BUTTON_SELECTOR = "div#detayPopUp.in a#btnKapatPencere_0.close"
DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000
def __init__(self, request_timeout: float = 60000):
self.playwright_instance: Optional[async_playwright] = None
self.browser: Optional[Browser] = None
self.context: Optional[BrowserContext] = None
self.page: Optional[Page] = None
self.request_timeout = request_timeout
self._lock = asyncio.Lock()
async def _ensure_playwright_ready(self, force_new_page: bool = False):
async with self._lock:
browser_recreated = False
context_recreated = False
if not self.playwright_instance:
self.playwright_instance = await async_playwright().start()
if not self.browser or not self.browser.is_connected():
if self.browser: await self.browser.close()
self.browser = await self.playwright_instance.chromium.launch(headless=True)
browser_recreated = True
if not self.context or browser_recreated:
if self.context: await self.context.close()
if not self.browser: raise PlaywrightError("Browser not initialized.")
self.context = await self.browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
java_script_enabled=True,
)
context_recreated = True
if not self.page or self.page.is_closed() or force_new_page or context_recreated or browser_recreated:
if self.page and not self.page.is_closed(): await self.page.close()
if not self.context: raise PlaywrightError("Context is None.")
self.page = await self.context.new_page()
if not self.page: raise PlaywrightError("Failed to create new page.")
self.page.set_default_navigation_timeout(self.request_timeout)
self.page.set_default_timeout(self.request_timeout)
if not self.page or self.page.is_closed():
raise PlaywrightError("Playwright page initialization failed.")
logger.debug("_ensure_playwright_ready completed.")
async def close_client_session(self):
async with self._lock:
# ... (öncekiyle aynı)
if self.page and not self.page.is_closed(): await self.page.close(); self.page = None
if self.context: await self.context.close(); self.context = None
if self.browser: await self.browser.close(); self.browser = None
if self.playwright_instance: await self.playwright_instance.stop(); self.playwright_instance = None
logger.info("KikApiClient (Playwright): Resources closed.")
def _parse_decision_entries_from_soup(self, soup: BeautifulSoup, search_karar_tipi: KikKararTipi) -> List[KikDecisionEntry]:
entries: List[KikDecisionEntry] = []
table = soup.find("table", {"id": self.RESULTS_TABLE_ID})
if not table: return entries
rows = table.find_all("tr")
for row_idx, row in enumerate(rows):
if row_idx < 2: continue
cells = row.find_all("td")
if len(cells) == 6:
try:
preview_button_tag = cells[0].find("a", id=re.compile(r"btnOnizle$"))
event_target = ""
if preview_button_tag and preview_button_tag.has_attr('href'):
match = re.search(r"__doPostBack\('([^']*)','([^']*)'\)", preview_button_tag['href'])
if match: event_target = match.group(1)
karar_no_span = cells[1].find("span", id=re.compile(r"lblKno$"))
karar_tarihi_span = cells[2].find("span", id=re.compile(r"lblKtar$"))
idare_span = cells[3].find("span", id=re.compile(r"lblIdare$"))
basvuru_sahibi_span = cells[4].find("span", id=re.compile(r"lblSikayetci$"))
ihale_span = cells[5].find("span", id=re.compile(r"lblIhale$"))
if not (event_target and karar_no_span and karar_tarihi_span): continue
# Karar tipini arama parametresinden alıyoruz, çünkü HTML'de direkt olarak bulunmuyor.
entry = KikDecisionEntry(
preview_event_target=event_target,
kararNo=karar_no_span.get_text(strip=True),
karar_tipi=search_karar_tipi, # Arama yapılan karar tipini ekle
kararTarihi=karar_tarihi_span.get_text(strip=True),
idare=idare_span.get_text(strip=True) if idare_span else None,
basvuruSahibi=basvuru_sahibi_span.get_text(strip=True) if basvuru_sahibi_span else None,
ihaleKonusu=ihale_span.get_text(strip=True) if ihale_span else None,
)
entries.append(entry)
except Exception as e:
logger.error(f"Error parsing a KIK decision entry row: {e}", exc_info=True)
return entries
def _parse_total_records_from_soup(self, soup: BeautifulSoup) -> int:
# ... (öncekiyle aynı) ...
try:
pager_div = soup.find("div", class_="gridToplamSayi")
if pager_div:
match = re.search(r"Toplam Kayıt Sayısı:(\d+)", pager_div.get_text(strip=True))
if match: return int(match.group(1))
except: pass
return 0
def _parse_current_page_from_soup(self, soup: BeautifulSoup) -> int:
# ... (öncekiyle aynı) ...
try:
pager_div = soup.find("div", class_="sayfalama")
if pager_div:
active_page_span = pager_div.find("span", class_="active")
if active_page_span: return int(active_page_span.get_text(strip=True))
except: pass
return 1
async def search_decisions(self, search_params: KikSearchRequest) -> KikSearchResult:
await self._ensure_playwright_ready()
page = self.page
search_url = f"{self.BASE_URL}{self.SEARCH_PAGE_PATH}"
try:
if page.url != search_url:
await page.goto(search_url, wait_until="networkidle", timeout=self.request_timeout)
search_button_selector = f"a[id='{self.FIELD_LOCATORS['search_button_id']}']"
await page.wait_for_selector(search_button_selector, state="visible", timeout=self.request_timeout)
current_karar_tipi_value = search_params.karar_tipi.value
radio_locator_selector = f"{self.FIELD_LOCATORS['karar_tipi_radio_group']}[value='{current_karar_tipi_value}']"
if not await page.locator(radio_locator_selector).is_checked():
js_target_radio = f"ctl00$ContentPlaceHolder1${current_karar_tipi_value}"
async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout):
await page.evaluate(f"javascript:__doPostBack('{js_target_radio}','')")
await page.wait_for_timeout(1000)
async def fill_if_value(selector_key: str, value: Optional[str]):
if value is not None: await page.fill(self.FIELD_LOCATORS[selector_key], value)
# Karar No'yu KİK sitesine göndermeden önce '_' -> '/' dönüşümü yap
karar_no_for_kik_form = None
if search_params.karar_no: # search_params.karar_no Claude'dan '_' ile gelmiş olabilir
karar_no_for_kik_form = search_params.karar_no.replace('_', '/')
logger.info(f"Using karar_no '{karar_no_for_kik_form}' (transformed from '{search_params.karar_no}') for KIK form.")
await fill_if_value('karar_metni', search_params.karar_metni)
await fill_if_value('karar_no', karar_no_for_kik_form) # Dönüştürülmüş halini kullan
# ... (diğer fill_if_value çağrıları aynı) ...
await fill_if_value('karar_tarihi_baslangic', search_params.karar_tarihi_baslangic)
await fill_if_value('karar_tarihi_bitis', search_params.karar_tarihi_bitis)
await fill_if_value('resmi_gazete_sayisi', search_params.resmi_gazete_sayisi)
await fill_if_value('resmi_gazete_tarihi', search_params.resmi_gazete_tarihi)
await fill_if_value('basvuru_konusu_ihale', search_params.basvuru_konusu_ihale)
await fill_if_value('basvuru_sahibi', search_params.basvuru_sahibi)
await fill_if_value('ihaleyi_yapan_idare', search_params.ihaleyi_yapan_idare)
if search_params.yil:
await page.select_option(self.FIELD_LOCATORS['yil'], value=search_params.yil)
action_is_search_button_click = (search_params.page == 1)
event_target_for_submit: str
if action_is_search_button_click:
event_target_for_submit = self.FIELD_LOCATORS['search_button_id']
else: # Pagination
page_link_ctl_number = search_params.page + 2
event_target_for_submit = f"ctl00$ContentPlaceHolder1$grdKurulKararSorguSonuc$ctl14$ctl{page_link_ctl_number:02d}"
try:
async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout):
if action_is_search_button_click:
await page.locator(search_button_selector).click()
else:
await page.evaluate(f"javascript:__doPostBack('{event_target_for_submit}','')")
except PlaywrightTimeoutError:
await page.wait_for_timeout(2000)
results_table_dom_selector = f"table#{self.RESULTS_TABLE_ID}"
try:
await page.wait_for_selector(results_table_dom_selector, timeout=30000, state="attached")
await page.wait_for_timeout(2000)
except PlaywrightTimeoutError:
logger.warning(f"Timeout waiting for results table '{results_table_dom_selector}'.")
html_content = await page.content()
soup = BeautifulSoup(html_content, "html.parser")
# ... (hata ve sonuç yok mesajı kontrolü aynı) ...
validation_summary_tag = soup.find("div", id=self.VALIDATION_SUMMARY_SELECTOR.split('[')[0].split(':')[0])
if validation_summary_tag and validation_summary_tag.get_text(strip=True) and \
("display: none" not in validation_summary_tag.get("style", "").lower() if validation_summary_tag.has_attr("style") else True) and \
validation_summary_tag.get_text(strip=True) != "":
return KikSearchResult(decisions=[], total_records=0, current_page=search_params.page)
message_content_div = soup.find("div", id=self.NO_RESULTS_MESSAGE_SELECTOR.split(':')[0])
if message_content_div and "kayıt bulunamamıştır" in message_content_div.get_text(strip=True).lower():
return KikSearchResult(decisions=[], total_records=0, current_page=1)
# _parse_decision_entries_from_soup'a arama yapılan karar_tipi'ni gönder
decisions = self._parse_decision_entries_from_soup(soup, search_params.karar_tipi)
total_records = self._parse_total_records_from_soup(soup)
current_page_from_html = self._parse_current_page_from_soup(soup)
return KikSearchResult(decisions=decisions, total_records=total_records, current_page=current_page_from_html)
except Exception as e:
logger.error(f"Error during KIK decision search: {e}", exc_info=True)
return KikSearchResult(decisions=[], current_page=search_params.page)
def _clean_html_for_markdown(self, html_content: str) -> str:
# ... (öncekiyle aynı) ...
if not html_content: return ""
return html_parser.unescape(html_content)
def _convert_html_to_markdown_internal(self, html_fragment: str) -> Optional[str]:
# ... (öncekiyle aynı) ...
if not html_fragment: return None
cleaned_html = self._clean_html_for_markdown(html_fragment)
markdown_output = None; temp_file_path = None
try:
md_converter = MarkItDown(enable_plugins=True, remove_alt_whitespace=True, keep_underline=True)
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html", encoding="utf-8") as tmp_html_file:
tmp_html_file.write(cleaned_html); temp_file_path = tmp_html_file.name
markdown_output = md_converter.convert(temp_file_path).text_content
if markdown_output: markdown_output = re.sub(r'\n{3,}', '\n\n', markdown_output).strip()
except Exception as e: logger.error(f"MarkItDown conversion error: {e}", exc_info=True)
finally:
if temp_file_path and os.path.exists(temp_file_path): os.remove(temp_file_path)
return markdown_output
async def get_decision_document_as_markdown(
self,
karar_id_b64: str,
page_number: int = 1
) -> KikDocumentMarkdown:
await self._ensure_playwright_ready()
# Bu metodun kendi içinde yeni bir 'page' nesnesi ('doc_page_for_content') kullanacağını unutmayın,
# ana 'self.page' arama sonuçları sayfasında kalır.
current_main_page = self.page # Ana arama sonuçları sayfasını referans alalım
try:
decoded_key = base64.b64decode(karar_id_b64.encode('utf-8')).decode('utf-8')
karar_tipi_value, karar_no_for_search = decoded_key.split('|', 1)
original_karar_tipi = KikKararTipi(karar_tipi_value)
logger.info(f"KIK Get Detail: Decoded karar_id '{karar_id_b64}' to Karar Tipi: {original_karar_tipi.value}, Karar No: {karar_no_for_search}. Requested Markdown Page: {page_number}")
except Exception as e_decode:
logger.error(f"Invalid karar_id format. Could not decode Base64 or split: {karar_id_b64}. Error: {e_decode}")
return KikDocumentMarkdown(retrieved_with_karar_id=karar_id_b64, error_message="Invalid karar_id format.", current_page=page_number)
default_error_response_data = {
"retrieved_with_karar_id": karar_id_b64,
"retrieved_karar_no": karar_no_for_search,
"retrieved_karar_tipi": original_karar_tipi,
"error_message": "An unspecified error occurred.",
"current_page": page_number, "total_pages": 1, "is_paginated": False
}
# Ana arama sayfasında olduğumuzdan emin olalım
if self.SEARCH_PAGE_PATH not in current_main_page.url:
logger.info(f"Not on search page ({current_main_page.url}). Navigating to {self.SEARCH_PAGE_PATH} before targeted search for document.")
await current_main_page.goto(f"{self.BASE_URL}{self.SEARCH_PAGE_PATH}", wait_until="networkidle", timeout=self.request_timeout)
await current_main_page.wait_for_selector(f"a[id='{self.FIELD_LOCATORS['search_button_id']}']", state="visible", timeout=self.request_timeout)
targeted_search_params = KikSearchRequest(
karar_no=karar_no_for_search,
karar_tipi=original_karar_tipi,
page=1
)
logger.info(f"Performing targeted search for Karar No: {karar_no_for_search}")
# search_decisions kendi içinde _ensure_playwright_ready çağırır ve self.page'i kullanır.
# Bu, current_main_page ile aynı olmalı.
search_results = await self.search_decisions(targeted_search_params)
if not search_results.decisions:
default_error_response_data["error_message"] = f"Decision with Karar No '{karar_no_for_search}' (Tipi: {original_karar_tipi.value}) not found by internal search."
return KikDocumentMarkdown(**default_error_response_data)
decision_to_fetch = None
for dec_entry in search_results.decisions:
if dec_entry.karar_no_str == karar_no_for_search and dec_entry.karar_tipi == original_karar_tipi:
decision_to_fetch = dec_entry
break
if not decision_to_fetch:
default_error_response_data["error_message"] = f"Karar No '{karar_no_for_search}' (Tipi: {original_karar_tipi.value}) not present with an exact match in first page of targeted search results."
return KikDocumentMarkdown(**default_error_response_data)
decision_preview_event_target = decision_to_fetch.preview_event_target
logger.info(f"Found target decision. Using preview_event_target: {decision_preview_event_target} for Karar No: {decision_to_fetch.karar_no_str}")
iframe_document_url_str = None
karar_id_param_from_url_on_doc_page = None
document_html_content = ""
try:
logger.info(f"Evaluating __doPostBack on main page to show modal for: {decision_preview_event_target}")
# Bu evaluate, self.page (yani current_main_page) üzerinde çalışır
await current_main_page.evaluate(f"javascript:__doPostBack('{decision_preview_event_target}','')")
await current_main_page.wait_for_timeout(1000)
logger.info(f"Executed __doPostBack for {decision_preview_event_target} on main page.")
iframe_selector = "iframe#iframe_detayPopUp"
modal_visible_selector = "div#detayPopUp.in"
try:
logger.info(f"Waiting for modal '{modal_visible_selector}' to be visible and iframe '{iframe_selector}' src to be populated on main page...")
await current_main_page.wait_for_function(
f"""
() => {{
const modal = document.querySelector('{modal_visible_selector}');
const iframe = document.querySelector('{iframe_selector}');
const modalIsTrulyVisible = modal && (window.getComputedStyle(modal).display !== 'none');
return modalIsTrulyVisible &&
iframe && iframe.getAttribute('src') &&
iframe.getAttribute('src').includes('KurulKararGoster.aspx');
}}
""",
timeout=self.request_timeout / 2
)
iframe_src_value = await current_main_page.locator(iframe_selector).get_attribute("src")
logger.info(f"Iframe src populated: {iframe_src_value}")
except PlaywrightTimeoutError:
logger.warning(f"Timeout waiting for KIK iframe src for {decision_preview_event_target}. Trying to parse from static content after presumed update.")
html_after_postback = await current_main_page.content()
# ... (fallback parsing öncekiyle aynı, default_error_response_data set edilir ve return edilir) ...
soup_after_postback = BeautifulSoup(html_after_postback, "html.parser")
detay_popup_div = soup_after_postback.find("div", {"id": "detayPopUp", "class": re.compile(r"\bin\b")})
if not detay_popup_div: detay_popup_div = soup_after_postback.find("div", {"id": "detayPopUp", "style": re.compile(r"display:\s*block", re.I)})
iframe_tag = detay_popup_div.find("iframe", {"id": "iframe_detayPopUp"}) if detay_popup_div else None
if iframe_tag and iframe_tag.has_attr("src") and iframe_tag["src"]: iframe_src_value = iframe_tag["src"]
else:
default_error_response_data["error_message"]="Timeout or failure finding decision content iframe URL after postback."
return KikDocumentMarkdown(**default_error_response_data)
if not iframe_src_value or not iframe_src_value.strip():
default_error_response_data["error_message"]="Extracted iframe URL for decision content is empty."
return KikDocumentMarkdown(**default_error_response_data)
# iframe_src_value göreceli bir URL ise, ana sayfanın URL'si ile birleştir
iframe_document_url_str = urllib.parse.urljoin(current_main_page.url, iframe_src_value)
logger.info(f"Constructed absolute iframe_document_url_str for goto: {iframe_document_url_str}") # Log this absolute URL
default_error_response_data["source_url"] = iframe_document_url_str
parsed_url = urllib.parse.urlparse(iframe_document_url_str)
query_params = urllib.parse.parse_qs(parsed_url.query)
karar_id_param_from_url_on_doc_page = query_params.get("KararId", [None])[0]
default_error_response_data["karar_id_param_from_url"] = karar_id_param_from_url_on_doc_page
if not karar_id_param_from_url_on_doc_page:
default_error_response_data["error_message"]="KararId (KIK internal ID) not found in extracted iframe URL."
return KikDocumentMarkdown(**default_error_response_data)
logger.info(f"Fetching KIK decision content from iframe URL using a new Playwright page: {iframe_document_url_str}")
doc_page_for_content = await self.context.new_page()
try:
# `goto` metoduna MUTLAK URL verilmeli. Loglanan URL'nin mutlak olduğundan emin olalım.
await doc_page_for_content.goto(iframe_document_url_str, wait_until="domcontentloaded", timeout=self.request_timeout)
document_html_content = await doc_page_for_content.content()
except Exception as e_doc_page:
logger.error(f"Error navigating or getting content from doc_page ({iframe_document_url_str}): {e_doc_page}")
if doc_page_for_content and not doc_page_for_content.is_closed(): await doc_page_for_content.close()
default_error_response_data["error_message"]=f"Failed to load decision detail page: {e_doc_page}"
return KikDocumentMarkdown(**default_error_response_data)
finally:
if doc_page_for_content and not doc_page_for_content.is_closed():
await doc_page_for_content.close()
soup_decision_detail = BeautifulSoup(document_html_content, "html.parser")
karar_content_span = soup_decision_detail.find("span", {"id": "ctl00_ContentPlaceHolder1_lblKarar"})
actual_decision_html = karar_content_span.decode_contents() if karar_content_span else document_html_content
full_markdown_content = self._convert_html_to_markdown_internal(actual_decision_html)
if not full_markdown_content:
default_error_response_data["error_message"]="Markdown conversion failed or returned empty content."
try:
if await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).is_visible(timeout=1000):
await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).click()
except: pass
return KikDocumentMarkdown(**default_error_response_data)
content_length = len(full_markdown_content); total_pages = math.ceil(content_length / self.DOCUMENT_MARKDOWN_CHUNK_SIZE) or 1
current_page_clamped = max(1, min(page_number, total_pages))
start_index = (current_page_clamped - 1) * self.DOCUMENT_MARKDOWN_CHUNK_SIZE
markdown_chunk = full_markdown_content[start_index : start_index + self.DOCUMENT_MARKDOWN_CHUNK_SIZE]
try:
if await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).is_visible(timeout=2000):
await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).click()
await current_main_page.wait_for_selector(f"div#detayPopUp:not(.in)", timeout=5000)
except: pass
return KikDocumentMarkdown(
retrieved_with_karar_id=karar_id_b64,
retrieved_karar_no=karar_no_for_search,
retrieved_karar_tipi=original_karar_tipi,
kararIdParam=karar_id_param_from_url_on_doc_page,
markdown_chunk=markdown_chunk, source_url=iframe_document_url_str,
current_page=current_page_clamped, total_pages=total_pages,
is_paginated=(total_pages > 1), full_content_char_count=content_length
)
except Exception as e:
logger.error(f"Error in get_decision_document_as_markdown for Karar ID {karar_id_b64}: {e}", exc_info=True)
default_error_response_data["error_message"] = f"General error: {str(e)}"
return KikDocumentMarkdown(**default_error_response_data)
+77
View File
@@ -0,0 +1,77 @@
# kik_mcp_module/models.py
from pydantic import BaseModel, Field, HttpUrl, computed_field
from typing import List, Optional
from enum import Enum
import base64 # Base64 encoding/decoding için
class KikKararTipi(str, Enum):
"""Enum for KIK (Public Procurement Authority) Decision Types."""
UYUSMAZLIK = "rbUyusmazlik"
DUZENLEYICI = "rbDuzenleyici"
MAHKEME = "rbMahkeme"
class KikSearchRequest(BaseModel):
"""Model for KIK Decision search criteria."""
karar_tipi: KikKararTipi = Field(KikKararTipi.UYUSMAZLIK, description="Type of KIK Decision.")
karar_no: Optional[str] = Field(None, description="Decision Number (e.g., '2024/UH.II-1766').")
karar_tarihi_baslangic: Optional[str] = Field(None, description="Decision Date Start (DD.MM.YYYY).", pattern=r"^\d{2}\.\d{2}\.\d{4}$")
karar_tarihi_bitis: Optional[str] = Field(None, description="Decision Date End (DD.MM.YYYY).", pattern=r"^\d{2}\.\d{2}\.\d{4}$")
resmi_gazete_sayisi: Optional[str] = Field(None, description="Official Gazette Number.")
resmi_gazete_tarihi: Optional[str] = Field(None, description="Official Gazette Date (DD.MM.YYYY).", pattern=r"^\d{2}\.\d{2}\.\d{4}$")
basvuru_konusu_ihale: Optional[str] = Field(None, description="Tender subject of the application.")
basvuru_sahibi: Optional[str] = Field(None, description="Applicant.")
ihaleyi_yapan_idare: Optional[str] = Field(None, description="Procuring Entity.")
yil: Optional[str] = Field(None, description="Year of the decision.")
karar_metni: Optional[str] = Field(None, description="Keyword/phrase in decision text.")
page: int = Field(1, ge=1, description="Results page number.")
class KikDecisionEntry(BaseModel):
"""Represents a single decision entry from KIK search results."""
preview_event_target: str = Field(..., description="Internal event target for fetching details.")
karar_no_str: str = Field(..., alias="kararNo", description="Raw decision number as extracted from KIK (e.g., '2024/UH.II-1766').")
karar_tipi: KikKararTipi = Field(..., description="The type of decision this entry belongs to.")
karar_tarihi_str: str = Field(..., alias="kararTarihi", description="Decision date.")
idare_str: Optional[str] = Field(None, alias="idare", description="Procuring entity.")
basvuru_sahibi_str: Optional[str] = Field(None, alias="basvuruSahibi", description="Applicant.")
ihale_konusu_str: Optional[str] = Field(None, alias="ihaleKonusu", description="Tender subject.")
@computed_field
@property
def karar_id(self) -> str:
"""
A Base64 encoded unique ID for the decision, combining decision type and number.
Format before encoding: "{karar_tipi.value}|{karar_no_str}"
"""
combined_key = f"{self.karar_tipi.value}|{self.karar_no_str}"
return base64.b64encode(combined_key.encode('utf-8')).decode('utf-8')
class Config:
populate_by_name = True
class KikSearchResult(BaseModel):
"""Model for KIK search results."""
decisions: List[KikDecisionEntry]
total_records: int = 0
current_page: int = 1
class KikDocumentMarkdown(BaseModel):
"""
KIK decision document, with Markdown content potentially paginated.
"""
retrieved_with_karar_id: Optional[str] = Field(None, description="The Base64 encoded karar_id that was used to request this document.")
# Decode edilmiş karar no ve tipini de yanıt olarak ekleyelim, Claude için faydalı olabilir.
retrieved_karar_no: Optional[str] = Field(None, description="The raw KIK Decision Number (e.g., '2024/UH.II-1766') this document pertains to.")
retrieved_karar_tipi: Optional[KikKararTipi] = Field(None, description="The KIK Decision Type this document pertains to.")
karar_id_param_from_url: Optional[str] = Field(None, alias="kararIdParam", description="The KIK system's internal KararId parameter from the document's display URL (KurulKararGoster.aspx).")
markdown_chunk: Optional[str] = Field(None, description="The requested chunk of the decision content converted to Markdown.")
source_url: Optional[str] = Field(None, description="The source URL of the original document (KurulKararGoster.aspx).")
error_message: Optional[str] = Field(None, description="Error message if document retrieval or processing failed.")
current_page: int = Field(1, description="The current page number of the markdown chunk being returned.")
total_pages: int = Field(1, description="The total number of pages the full markdown content is divided into.")
is_paginated: bool = Field(False, description="True if the full markdown content is split into multiple pages.")
full_content_char_count: Optional[int] = Field(None, description="Total character count of the full markdown content before chunking.")
class Config:
populate_by_name = True
+147 -68
View File
@@ -1,11 +1,11 @@
# mcp_server_main.py # mcp_server_main.py
import asyncio import asyncio
import atexit import atexit
import logging import logging
import os import os
from pydantic import HttpUrl from pydantic import HttpUrl, Field
from typing import Optional from typing import Optional, Dict
import urllib.parse # urllib.parse client tarafında kullanılıyor, server'da gerekmeyebilir.
# --- Logging Configuration Start --- # --- Logging Configuration Start ---
LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
@@ -32,7 +32,6 @@ logger = logging.getLogger(__name__)
# --- Logging Configuration End --- # --- Logging Configuration End ---
from fastmcp import FastMCP from fastmcp import FastMCP
from pydantic import Field
# --- Module Imports --- # --- Module Imports ---
from yargitay_mcp_module.client import YargitayOfficialApiClient from yargitay_mcp_module.client import YargitayOfficialApiClient
@@ -52,21 +51,30 @@ from uyusmazlik_mcp_module.client import UyusmazlikApiClient
from uyusmazlik_mcp_module.models import ( from uyusmazlik_mcp_module.models import (
UyusmazlikSearchRequest, UyusmazlikSearchResponse, UyusmazlikDocumentMarkdown UyusmazlikSearchRequest, UyusmazlikSearchResponse, UyusmazlikDocumentMarkdown
) )
from anayasa_mcp_module.client import AnayasaMahkemesiApiClient # Norm Denetimi Client from anayasa_mcp_module.client import AnayasaMahkemesiApiClient
from anayasa_mcp_module.bireysel_client import AnayasaBireyselBasvuruApiClient # Bireysel Başvuru Client from anayasa_mcp_module.bireysel_client import AnayasaBireyselBasvuruApiClient
from anayasa_mcp_module.models import ( from anayasa_mcp_module.models import (
AnayasaNormDenetimiSearchRequest, AnayasaNormDenetimiSearchRequest,
AnayasaSearchResult, AnayasaSearchResult,
AnayasaDocumentMarkdown, # For Norm Denetimi documents AnayasaDocumentMarkdown,
AnayasaBireyselReportSearchRequest, # For Bireysel Başvuru reports AnayasaBireyselReportSearchRequest,
AnayasaBireyselReportSearchResult, # For Bireysel Başvuru reports AnayasaBireyselReportSearchResult,
AnayasaBireyselBasvuruDocumentMarkdown, # For Bireysel Başvuru documents AnayasaBireyselBasvuruDocumentMarkdown,
) )
# KIK Module Imports
from kik_mcp_module.client import KikApiClient
from kik_mcp_module.models import (
KikKararTipi,
KikSearchRequest,
KikSearchResult,
KikDocumentMarkdown
)
app = FastMCP( app = FastMCP(
name="TurkishLawResearchAssistantMCP", name="TurkishLawResearchAssistantMCP",
instructions="MCP server for TR legal databases (Yargitay, Danistay, Emsal, Uyusmazlik, Anayasa-Norm, Anayasa-Bireysel).", instructions="MCP server for TR legal databases (Yargitay, Danistay, Emsal, Uyusmazlik, Anayasa-Norm, Anayasa-Bireysel, KIK).",
dependencies=["httpx", "beautifulsoup4", "markitdown", "pydantic", "aiohttp"] dependencies=["httpx", "beautifulsoup4", "markitdown", "pydantic", "aiohttp", "playwright"]
) )
# --- API Client Instances --- # --- API Client Instances ---
@@ -76,8 +84,10 @@ emsal_client_instance = EmsalApiClient()
uyusmazlik_client_instance = UyusmazlikApiClient() uyusmazlik_client_instance = UyusmazlikApiClient()
anayasa_norm_client_instance = AnayasaMahkemesiApiClient() anayasa_norm_client_instance = AnayasaMahkemesiApiClient()
anayasa_bireysel_client_instance = AnayasaBireyselBasvuruApiClient() anayasa_bireysel_client_instance = AnayasaBireyselBasvuruApiClient()
kik_client_instance = KikApiClient()
# --- MCP Tools for Yargitay --- # --- MCP Tools for Yargitay ---
# ... (Yargıtay araçları öncekiyle aynı, docstringler İngilizce) ...
@app.tool() @app.tool()
async def search_yargitay_detailed(search_query: YargitayDetailedSearchRequest) -> CompactYargitaySearchResult: async def search_yargitay_detailed(search_query: YargitayDetailedSearchRequest) -> CompactYargitaySearchResult:
"""Searches Yargitay (Court of Cassation) decisions using detailed criteria.""" """Searches Yargitay (Court of Cassation) decisions using detailed criteria."""
@@ -93,7 +103,7 @@ async def search_yargitay_detailed(search_query: YargitayDetailedSearchRequest)
logger.warning("API response for Yargitay search did not contain expected data structure.") logger.warning("API response for Yargitay search did not contain expected data structure.")
return CompactYargitaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize) return CompactYargitaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_yargitay_detailed' with query: {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_yargitay_detailed'.") # Query loglaması kaldırıldı, PII içerebilir
raise raise
@app.tool() @app.tool()
@@ -104,14 +114,15 @@ async def get_yargitay_document_markdown(document_id: str) -> YargitayDocumentMa
try: try:
return await yargitay_client_instance.get_decision_document_as_markdown(document_id) return await yargitay_client_instance.get_decision_document_as_markdown(document_id)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_yargitay_document_markdown' for ID: {document_id}") logger.exception(f"Error in tool 'get_yargitay_document_markdown'.")
raise raise
# --- MCP Tools for Danistay --- # --- MCP Tools for Danistay ---
# ... (Danıştay araçları öncekiyle aynı, docstringler İngilizce) ...
@app.tool() @app.tool()
async def search_danistay_by_keyword(search_query: DanistayKeywordSearchRequest) -> CompactDanistaySearchResult: async def search_danistay_by_keyword(search_query: DanistayKeywordSearchRequest) -> CompactDanistaySearchResult:
"""Searches Danıştay (Council of State) decisions using keywords.""" """Searches Danıştay (Council of State) decisions using keywords."""
logger.info(f"Tool 'search_danistay_by_keyword' called: {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.info(f"Tool 'search_danistay_by_keyword' called.") # Query loglaması kaldırıldı
try: try:
api_response = await danistay_client_instance.search_keyword_decisions(search_query) api_response = await danistay_client_instance.search_keyword_decisions(search_query)
if api_response.data: if api_response.data:
@@ -123,13 +134,13 @@ async def search_danistay_by_keyword(search_query: DanistayKeywordSearchRequest)
logger.warning("API response for Danistay keyword search did not contain expected data structure.") logger.warning("API response for Danistay keyword search did not contain expected data structure.")
return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize) return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_danistay_by_keyword': {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_danistay_by_keyword'.")
raise raise
@app.tool() @app.tool()
async def search_danistay_detailed(search_query: DanistayDetailedSearchRequest) -> CompactDanistaySearchResult: async def search_danistay_detailed(search_query: DanistayDetailedSearchRequest) -> CompactDanistaySearchResult:
"""Performs a detailed search for Danıştay (Council of State) decisions.""" """Performs a detailed search for Danıştay (Council of State) decisions."""
logger.info(f"Tool 'search_danistay_detailed' called: {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.info(f"Tool 'search_danistay_detailed' called.") # Query loglaması kaldırıldı
try: try:
api_response = await danistay_client_instance.search_detailed_decisions(search_query) api_response = await danistay_client_instance.search_detailed_decisions(search_query)
if api_response.data: if api_response.data:
@@ -141,7 +152,7 @@ async def search_danistay_detailed(search_query: DanistayDetailedSearchRequest)
logger.warning("API response for Danistay detailed search did not contain expected data structure.") logger.warning("API response for Danistay detailed search did not contain expected data structure.")
return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize) return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_danistay_detailed': {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_danistay_detailed'.")
raise raise
@app.tool() @app.tool()
@@ -152,14 +163,15 @@ async def get_danistay_document_markdown(document_id: str) -> DanistayDocumentMa
try: try:
return await danistay_client_instance.get_decision_document_as_markdown(document_id) return await danistay_client_instance.get_decision_document_as_markdown(document_id)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_danistay_document_markdown' for ID: {document_id}") logger.exception(f"Error in tool 'get_danistay_document_markdown'.")
raise raise
# --- MCP Tools for Emsal --- # --- MCP Tools for Emsal ---
# ... (Emsal araçları öncekiyle aynı, docstringler İngilizce) ...
@app.tool() @app.tool()
async def search_emsal_detailed_decisions(search_query: EmsalSearchRequest) -> CompactEmsalSearchResult: async def search_emsal_detailed_decisions(search_query: EmsalSearchRequest) -> CompactEmsalSearchResult:
"""Searches for Emsal (UYAP Precedent) decisions using detailed criteria.""" """Searches for Emsal (UYAP Precedent) decisions using detailed criteria."""
logger.info(f"Tool 'search_emsal_detailed_decisions' called: {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.info(f"Tool 'search_emsal_detailed_decisions' called.") # Query loglaması kaldırıldı
try: try:
api_response = await emsal_client_instance.search_detailed_decisions(search_query) api_response = await emsal_client_instance.search_detailed_decisions(search_query)
if api_response.data: if api_response.data:
@@ -172,7 +184,7 @@ async def search_emsal_detailed_decisions(search_query: EmsalSearchRequest) -> C
logger.warning("API response for Emsal search did not contain expected data structure.") logger.warning("API response for Emsal search did not contain expected data structure.")
return CompactEmsalSearchResult(decisions=[], total_records=0, requested_page=search_query.page_number, page_size=search_query.page_size) return CompactEmsalSearchResult(decisions=[], total_records=0, requested_page=search_query.page_number, page_size=search_query.page_size)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_emsal_detailed_decisions': {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_emsal_detailed_decisions'.")
raise raise
@app.tool() @app.tool()
@@ -183,20 +195,21 @@ async def get_emsal_document_markdown(document_id: str) -> EmsalDocumentMarkdown
try: try:
return await emsal_client_instance.get_decision_document_as_markdown(document_id) return await emsal_client_instance.get_decision_document_as_markdown(document_id)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_emsal_document_markdown' for ID: {document_id}") logger.exception(f"Error in tool 'get_emsal_document_markdown'.")
raise raise
# --- MCP Tools for Uyusmazlik --- # --- MCP Tools for Uyusmazlik ---
# ... (Uyuşmazlık araçları öncekiyle aynı, docstringler İngilizce) ...
@app.tool() @app.tool()
async def search_uyusmazlik_decisions( async def search_uyusmazlik_decisions(
search_params: UyusmazlikSearchRequest search_params: UyusmazlikSearchRequest
) -> UyusmazlikSearchResponse: ) -> UyusmazlikSearchResponse:
"""Searches for Uyuşmazlık Mahkemesi decisions using various criteria from the form.""" """Searches for Uyuşmazlık Mahkemesi (Court of Jurisdictional Disputes) decisions using various criteria."""
logger.info(f"Tool 'search_uyusmazlik_decisions' called with params: {search_params.model_dump_json(exclude_none=True, indent=2)}") logger.info(f"Tool 'search_uyusmazlik_decisions' called.") # Query loglaması kaldırıldı
try: try:
return await uyusmazlik_client_instance.search_decisions(search_params) return await uyusmazlik_client_instance.search_decisions(search_params)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_uyusmazlik_decisions': {search_params.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_uyusmazlik_decisions'.")
raise raise
@app.tool() @app.tool()
@@ -212,34 +225,35 @@ async def get_uyusmazlik_document_markdown_from_url(document_url: HttpUrl) -> Uy
try: try:
return await uyusmazlik_client_instance.get_decision_document_as_markdown(str(document_url)) return await uyusmazlik_client_instance.get_decision_document_as_markdown(str(document_url))
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url' for URL: {str(document_url)}") logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url'.")
raise raise
# --- MCP Tools for Anayasa Mahkemesi (Norm Denetimi) --- # --- MCP Tools for Anayasa Mahkemesi (Norm Denetimi) ---
# ... (Anayasa Norm araçları öncekiyle aynı, docstringler İngilizce) ...
@app.tool() @app.tool()
async def search_anayasa_norm_denetimi_decisions( async def search_anayasa_norm_denetimi_decisions(
search_query: AnayasaNormDenetimiSearchRequest search_query: AnayasaNormDenetimiSearchRequest
) -> AnayasaSearchResult: ) -> AnayasaSearchResult:
""" """
Searches Anayasa Mahkemesi (Constitutional Court) Norm Denetimi decisions Searches Anayasa Mahkemesi (Constitutional Court) Norm Denetimi (Norm Control) decisions
using various criteria from the official search form. This is for https://normkararlarbilgibankasi.anayasa.gov.tr. using criteria from https://normkararlarbilgibankasi.anayasa.gov.tr.
""" """
logger.info(f"Tool 'search_anayasa_norm_denetimi_decisions' called: {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.info(f"Tool 'search_anayasa_norm_denetimi_decisions' called.") # Query loglaması kaldırıldı
try: try:
return await anayasa_norm_client_instance.search_norm_denetimi_decisions(search_query) return await anayasa_norm_client_instance.search_norm_denetimi_decisions(search_query)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_anayasa_norm_denetimi_decisions': {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_anayasa_norm_denetimi_decisions'.")
raise raise
@app.tool() @app.tool()
async def get_anayasa_norm_denetimi_document_markdown( async def get_anayasa_norm_denetimi_document_markdown(
document_url: str = Field(..., description="The URL path of the AYM Norm Denetimi decision (e.g., /ND/YYYY/NN) or full https URL from normkararlarbilgibankasi.anayasa.gov.tr."), document_url: str = Field(..., description="The URL path (e.g., /ND/YYYY/NN) or full https URL of the AYM Norm Denetimi decision from normkararlarbilgibankasi.anayasa.gov.tr."),
page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content, 1-indexed. Default is 1 for the first 5,000 characters.") # Corrected chunk size in description page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed). Default is 1 (first 5,000 characters).")
) -> AnayasaDocumentMarkdown: ) -> AnayasaDocumentMarkdown:
""" """
Retrieves a specific Anayasa Mahkemesi (Norm Denetimi) decision Retrieves a specific Anayasa Mahkemesi (Norm Denetimi) decision
from its URL and returns its content as paginated Markdown. This is for https://normkararlarbilgibankasi.anayasa.gov.tr. from its URL and returns its content as paginated Markdown.
Content is paginated if it exceeds 5,000 characters. Use 'page_number' to get subsequent pages. Content is paginated if it exceeds 5,000 characters. Use 'page_number' for subsequent pages.
""" """
logger.info(f"Tool 'get_anayasa_norm_denetimi_document_markdown' called for URL: {document_url}, Page: {page_number}") logger.info(f"Tool 'get_anayasa_norm_denetimi_document_markdown' called for URL: {document_url}, Page: {page_number}")
if not document_url or not document_url.strip(): if not document_url or not document_url.strip():
@@ -248,10 +262,11 @@ async def get_anayasa_norm_denetimi_document_markdown(
try: try:
return await anayasa_norm_client_instance.get_decision_document_as_markdown(document_url, page_number=current_page_to_fetch) return await anayasa_norm_client_instance.get_decision_document_as_markdown(document_url, page_number=current_page_to_fetch)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_anayasa_norm_denetimi_document_markdown' for URL: {document_url}, Page: {current_page_to_fetch}") logger.exception(f"Error in tool 'get_anayasa_norm_denetimi_document_markdown'.")
raise raise
# --- MCP Tools for Anayasa Mahkemesi (Bireysel Başvuru Karar Raporu & Belgeler) --- # --- MCP Tools for Anayasa Mahkemesi (Bireysel Başvuru Karar Raporu & Belgeler) ---
# ... (Anayasa Bireysel araçları öncekiyle aynı, docstringler İngilizce) ...
@app.tool() @app.tool()
async def search_anayasa_bireysel_basvuru_report( async def search_anayasa_bireysel_basvuru_report(
search_query: AnayasaBireyselReportSearchRequest search_query: AnayasaBireyselReportSearchRequest
@@ -259,72 +274,138 @@ async def search_anayasa_bireysel_basvuru_report(
""" """
Searches Anayasa Mahkemesi (Constitutional Court) Bireysel Başvuru (Individual Application) Searches Anayasa Mahkemesi (Constitutional Court) Bireysel Başvuru (Individual Application)
decisions and generates a 'Karar Arama Raporu' (Decision Search Report). decisions and generates a 'Karar Arama Raporu' (Decision Search Report).
This is for https://kararlarbilgibankasi.anayasa.gov.tr and uses the KararBulteni=1 parameter. This is for https://kararlarbilgibankasi.anayasa.gov.tr (uses KararBulteni=1).
The report typically displays 10 decisions per page by default.
""" """
logger.info(f"Tool 'search_anayasa_bireysel_basvuru_report' called: {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.info(f"Tool 'search_anayasa_bireysel_basvuru_report' called.") # Query loglaması kaldırıldı
try: try:
return await anayasa_bireysel_client_instance.search_bireysel_basvuru_report(search_query) return await anayasa_bireysel_client_instance.search_bireysel_basvuru_report(search_query)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_anayasa_bireysel_basvuru_report': {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_anayasa_bireysel_basvuru_report'.")
raise raise
@app.tool() @app.tool()
async def get_anayasa_bireysel_basvuru_document_markdown( async def get_anayasa_bireysel_basvuru_document_markdown(
document_url_path: str = Field(..., description="The URL path of the AYM Bireysel Başvuru decision (e.g., /BB/YYYY/NNNN) from kararlarbilgibankasi.anayasa.gov.tr."), document_url_path: str = Field(..., description="The URL path (e.g., /BB/YYYY/NNNN) of the AYM Bireysel Başvuru decision from kararlarbilgibankasi.anayasa.gov.tr."),
page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content, 1-indexed. Default is 1 for the first 5,000 characters.") page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed). Default is 1 (first 5,000 characters).")
) -> AnayasaBireyselBasvuruDocumentMarkdown: ) -> AnayasaBireyselBasvuruDocumentMarkdown:
""" """
Retrieves a specific Anayasa Mahkemesi Bireysel Başvuru (Individual Application) decision Retrieves a specific Anayasa Mahkemesi Bireysel Başvuru (Individual Application) decision
from its URL path (e.g., /BB/YYYY/NNNN found in report results) and returns its content as paginated Markdown. from its URL path (e.g., /BB/YYYY/NNNN) and returns content as paginated Markdown.
This is for https://kararlarbilgibankasi.anayasa.gov.tr. Content is paginated if it exceeds 5,000 characters. Use 'page_number' for subsequent pages.
Content is paginated if it exceeds 5,000 characters. Use 'page_number' to get subsequent pages.
""" """
logger.info(f"Tool 'get_anayasa_bireysel_basvuru_document_markdown' called for URL path: {document_url_path}, Page: {page_number}") logger.info(f"Tool 'get_anayasa_bireysel_basvuru_document_markdown' called for URL path: {document_url_path}, Page: {page_number}")
if not document_url_path or not document_url_path.strip() or not document_url_path.startswith("/BB/"): if not document_url_path or not document_url_path.strip() or not document_url_path.startswith("/BB/"):
raise ValueError("Document URL path (e.g., /BB/YYYY/NNNN) is required for Anayasa Bireysel Başvuru document retrieval.") raise ValueError("Document URL path (e.g., /BB/YYYY/NNNN) is required for Anayasa Bireysel Başvuru document retrieval.")
current_page_to_fetch = page_number if page_number is not None and page_number >= 1 else 1
try:
return await anayasa_bireysel_client_instance.get_decision_document_as_markdown(document_url_path, page_number=current_page_to_fetch)
except Exception as e:
logger.exception(f"Error in tool 'get_anayasa_bireysel_basvuru_document_markdown'.")
raise
# --- MCP Tools for KIK (Kamu İhale Kurulu) ---
@app.tool()
async def search_kik_decisions(search_query: KikSearchRequest) -> KikSearchResult:
"""
Searches KIK (Public Procurement Authority) decisions.
"""
logger.info(f"Tool 'search_kik_decisions' called.") # Query loglaması kaldırıldı
try:
api_response = await kik_client_instance.search_decisions(search_query)
page_param_for_log = search_query.page if hasattr(search_query, 'page') else 1
if not api_response.decisions and api_response.total_records == 0 and page_param_for_log == 1:
logger.warning(f"KIK search returned no decisions for query.") # Query detayı kaldırıldı
return api_response
except Exception as e:
logger.exception(f"Error in KIK search tool 'search_kik_decisions'.")
current_page_val = search_query.page if hasattr(search_query, 'page') else 1
return KikSearchResult(decisions=[], total_records=0, current_page=current_page_val)
@app.tool()
async def get_kik_document_markdown(
karar_id: str = Field(..., description="The Base64 encoded KIK decision identifier."),
page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed). Default is 1.")
) -> KikDocumentMarkdown:
"""
Retrieves a specific KIK (Public Procurement Authority) decision using its Base64 encoded 'karar_id'.
Content is returned as paginated Markdown.
"""
logger.info(f"Tool 'get_kik_document_markdown' called for KIK karar_id: {karar_id}, Markdown Page: {page_number}")
if not karar_id or not karar_id.strip():
logger.error("KIK Document retrieval: karar_id cannot be empty.")
return KikDocumentMarkdown(
retrieved_with_karar_id=karar_id,
error_message="karar_id is required and must be a non-empty string.",
current_page=page_number or 1,
total_pages=1,
is_paginated=False
)
current_page_to_fetch = page_number if page_number is not None and page_number >= 1 else 1 current_page_to_fetch = page_number if page_number is not None and page_number >= 1 else 1
try: try:
return await anayasa_bireysel_client_instance.get_decision_document_as_markdown(document_url_path, page_number=current_page_to_fetch) return await kik_client_instance.get_decision_document_as_markdown(
karar_id_b64=karar_id,
page_number=current_page_to_fetch
)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_anayasa_bireysel_basvuru_document_markdown' for URL path: {document_url_path}, Page: {current_page_to_fetch}") logger.exception(f"Error in KIK document retrieval tool 'get_kik_document_markdown' for karar_id: {karar_id}")
raise return KikDocumentMarkdown(
retrieved_with_karar_id=karar_id,
error_message=f"Tool-level error during KIK document retrieval: {str(e)}",
current_page=current_page_to_fetch,
total_pages=1,
is_paginated=False
)
# --- Application Shutdown Handling --- # --- Application Shutdown Handling ---
# ... (perform_cleanup ve main fonksiyonları öncekiyle aynı, değişiklik yok) ...
def perform_cleanup(): def perform_cleanup():
logger.info("MCP Server performing cleanup...") logger.info("MCP Server performing cleanup...")
try: try:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop_policy().get_event_loop()
if loop.is_closed(): if loop.is_closed():
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
except RuntimeError: # pragma: no cover except RuntimeError:
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
clients_to_close = [
try: globals().get('yargitay_client_instance'),
clients_to_close = [ globals().get('danistay_client_instance'),
globals().get('yargitay_client_instance'), globals().get('emsal_client_instance'),
globals().get('danistay_client_instance'), globals().get('uyusmazlik_client_instance'),
globals().get('emsal_client_instance'), globals().get('anayasa_norm_client_instance'),
globals().get('uyusmazlik_client_instance'), globals().get('anayasa_bireysel_client_instance'),
globals().get('anayasa_norm_client_instance'), globals().get('kik_client_instance')
globals().get('anayasa_bireysel_client_instance') ]
] async def close_all_clients_async():
tasks = []
for client_instance in clients_to_close: for client_instance in clients_to_close:
if client_instance and hasattr(client_instance, 'close_client_session') and callable(client_instance.close_client_session): if client_instance and hasattr(client_instance, 'close_client_session') and callable(client_instance.close_client_session):
logger.info(f"Closing client session for {client_instance.__class__.__name__} via atexit.") logger.info(f"Scheduling close for client session: {client_instance.__class__.__name__}")
loop.run_until_complete(client_instance.close_client_session()) tasks.append(client_instance.close_client_session())
if tasks:
results = await asyncio.gather(*tasks, return_exceptions=True)
for i, result in enumerate(results):
if isinstance(result, Exception):
client_name = "Unknown Client"
if i < len(clients_to_close) and clients_to_close[i] is not None:
client_name = clients_to_close[i].__class__.__name__
logger.error(f"Error closing client {client_name}: {result}")
try:
if loop.is_running():
asyncio.ensure_future(close_all_clients_async(), loop=loop)
logger.info("Client cleanup tasks scheduled on running event loop.")
else:
loop.run_until_complete(close_all_clients_async())
logger.info("Client cleanup tasks completed via run_until_complete.")
except Exception as e: except Exception as e:
logger.error(f"Error during atexit cleanup: {e}") logger.error(f"Error during atexit cleanup execution: {e}", exc_info=True)
logger.info("MCP Server atexit cleanup attempt finished.") logger.info("MCP Server atexit cleanup process finished.")
atexit.register(perform_cleanup) atexit.register(perform_cleanup)
def main(): def main():
"""Main entry point for the MCP server."""
logger.info(f"Starting {app.name} server via main() function...") logger.info(f"Starting {app.name} server via main() function...")
logger.info(f"Logs will be written to: {LOG_FILE_PATH}") logger.info(f"Logs will be written to: {LOG_FILE_PATH}")
try: try:
@@ -335,7 +416,5 @@ def main():
logger.exception("Server failed to start or crashed.") logger.exception("Server failed to start or crashed.")
finally: finally:
logger.info(f"{app.name} server has shut down.") logger.info(f"{app.name} server has shut down.")
# --- Main Entry Point ---
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+1
View File
@@ -11,6 +11,7 @@ dependencies = [
"markitdown>=0.1.1", "markitdown>=0.1.1",
"pydantic>=2.11.4", "pydantic>=2.11.4",
"aiohttp>=3.11.18", "aiohttp>=3.11.18",
"playwright>=1.52.0",
] ]
[project.scripts] [project.scripts]