From 3927dcee8fbd21824479d42632944bf36c56de70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Burak=20Ta=C5=9Fy=C3=BCrek?= Date: Sat, 13 Jun 2026 00:07:03 +0300 Subject: [PATCH] feat(deep-research): reduce Bedesten request overhead Use search-result metadata for Deep Research previews instead of fetching every candidate document. This keeps the compatibility tools within upstream Bedesten rate limits and updates the README to match the current active tool set. --- README.md | 27 +++--- mcp_server_main.py | 221 +++++++++++++++++---------------------------- 2 files changed, 97 insertions(+), 151 deletions(-) diff --git a/README.md b/README.md index 939fe76..af1a8d4 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ API anahtarınızı [openrouter.ai/keys](https://openrouter.ai/keys) adresinden
🛠️ Kullanılabilir Araçlar (MCP Tools) -Bu FastMCP sunucusu **24 temel MCP aracı** + **1 opsiyonel semantik arama aracı** sunar (token verimliliği için optimize edilmiş): +Bu FastMCP sunucusu **26 aktif MCP aracı** + **1 opsiyonel semantik arama aracı** sunar (token verimliliği için optimize edilmiş): ### **Yargıtay Araçları (Birleşik Bedesten API - Token Optimized)** *Not: Yargıtay araçları token verimliliği için birleşik Bedesten API'ye entegre edilmiştir* @@ -338,8 +338,8 @@ Bu FastMCP sunucusu **24 temel MCP aracı** + **1 opsiyonel semantik arama arac 8. `get_anayasa_document_unified(document_url, page_number)`: AYM kararlarını birleşik belge getirme - **sayfalanmış Markdown** içeriği ### **KİK (Kamu İhale Kurulu) Araçları** -9. `search_kik_decisions(karar_tipi, ...)`: KİK (Kamu İhale Kurulu) kararlarını arar. -10. `get_kik_document_markdown(karar_id, page_number)`: Belirli bir KİK kararını, Base64 ile encode edilmiş `karar_id`'sini kullanarak alır ve **sayfalanmış Markdown** içeriğini getirir. +9. `search_kik_v2_decisions(decision_type, karar_metni, karar_no, basvuran, idare_adi, baslangic_tarihi, bitis_tarihi)`: KİK v2 API ile uyuşmazlık, düzenleyici ve mahkeme kararlarını arar. +10. `get_kik_v2_document_markdown(gundemMaddesiId)`: Arama sonucundaki `gundemMaddesiId` ile KİK karar metnini Markdown formatında getirir. ### **Rekabet Kurumu Araçları**     * `search_rekabet_kurumu_decisions(KararTuru: Literal[...], ...) -> RekabetSearchResult`: Rekabet Kurumu kararlarını arar. `KararTuru` için kullanıcı dostu isimler kullanılır (örn: "Birleşme ve Devralma").     * `get_rekabet_kurumu_document(karar_id: str, page_number: Optional[int] = 1) -> RekabetDocument`: Belirli bir Rekabet Kurumu kararını `karar_id` ile alır. Kararın PDF formatındaki orijinalinden istenen sayfayı ayıklar ve Markdown formatında döndürür. @@ -347,16 +347,12 @@ Bu FastMCP sunucusu **24 temel MCP aracı** + **1 opsiyonel semantik arama arac --- -* **Sayıştay Araçları (3 Karar Türü + 8 Daire Filtreleme):** - * `search_sayistay_genel_kurul(karar_no, karar_tarih_baslangic, karar_tamami, ...)`: Sayıştay Genel Kurul (yorumlayıcı) kararlarını arar. **Tarih aralığı** (2006-2024) + **İçerik arama** (400 karakter) - * `search_sayistay_temyiz_kurulu(ilam_dairesi, kamu_idaresi_turu, temyiz_karar, ...)`: Temyiz Kurulu (itiraz) kararlarını arar. **8 Daire filtreleme** + **Kurum türü** + **Konu sınıflandırması** - * `search_sayistay_daire(yargilama_dairesi, web_karar_metni, hesap_yili, ...)`: Daire (ilk derece denetim) kararlarını arar. **8 Daire filtreleme** + **Hesap yılı** + **İçerik arama** - * `get_sayistay_genel_kurul_document_markdown(decision_id: str)`: Genel Kurul kararının tam metnini Markdown formatında getirir - * `get_sayistay_temyiz_kurulu_document_markdown(decision_id: str)`: Temyiz Kurulu kararının tam metnini Markdown formatında getirir - * `get_sayistay_daire_document_markdown(decision_id: str)`: Daire kararının tam metnini Markdown formatında getirir +* **Sayıştay Araçları (Birleşik API, 3 Karar Türü + 8 Daire Filtreleme):** + * `search_sayistay_unified(decision_type, start, length, ...)`: `genel_kurul`, `temyiz_kurulu` veya `daire` kararlarını tek araçla arar. `length` 1-100 aralığındadır. + * `get_sayistay_document_unified(decision_id, decision_type)`: Birleşik arama sonucundaki karar ID'si ve karar türüyle tam metni Markdown formatında getirir. * **KVKK Araçları (Brave Search API + Türkçe Arama):** - * `search_kvkk_decisions(keywords, page, pageSize, ...)`: KVKK (Kişisel Verilerin Korunması Kurulu) kararlarını Brave Search API ile arar. **Türkçe arama** + **Site hedeflemeli** (`site:kvkk.gov.tr "karar özeti"`) + **Sayfalama desteği** + * `search_kvkk_decisions(keywords, page)`: KVKK (Kişisel Verilerin Korunması Kurulu) kararlarını Brave Search API ile arar. **Türkçe arama** + **Site hedeflemeli** (`site:kvkk.gov.tr "karar özeti"`) + **Sayfalama desteği**. Sonuç sayısı sunucuda 10 olarak sabitlenmiştir. * `get_kvkk_document_markdown(decision_url: str, page_number: Optional[int] = 1)`: KVKK kararının tam metnini **sayfalanmış Markdown** formatında getirir (5.000 karakterlik sayfa) ### BDDK Araçları @@ -368,10 +364,15 @@ Bu FastMCP sunucusu **24 temel MCP aracı** + **1 opsiyonel semantik arama arac * `get_gib_ozelge_document_markdown(ozelge_id: int, page_number: int = 1)`: Belirli bir özelgenin tam metnini **sayfalanmış Markdown** formatında getirir (5.000 karakterlik sayfa) + **Metadata başlık bloğu** (Başlık, Sayı, Tarih, Kanun, Kaynak URL) ### Sigorta Tahkim Komisyonu Araçları (Tavily Search API + PDF) - * `search_sigorta_tahkim_decisions(keywords, page, pageSize)`: Sigorta Tahkim Komisyonu kararlarını Tavily Search API ile arar. **Site hedeflemeli** (`sigortatahkim.org`) + **Sayfalama desteği** + * `search_sigorta_tahkim_decisions(keywords, page)`: Sigorta Tahkim Komisyonu kararlarını Tavily Search API ile arar. **Site hedeflemeli** (`sigortatahkim.org`) + **Sayfalama desteği**. Sonuç sayısı sunucuda 10 olarak sabitlenmiştir. * `get_sigorta_tahkim_document_markdown(issue_number: str, page_number: int)`: Hakem Karar Dergisi sayısının PDF'ini indirip **sayfalanmış Markdown** formatında getirir (5.000 karakterlik sayfa). 64 sayı (2010-2025) * `search_within_sigorta_tahkim_issue(issue_number: str, keyword: str, max_results: int)`: Belirli bir dergi sayısı içindeki kararları anahtar kelime ile arar. **Türkçe İ/I desteği** + **Relevance scoring** + **Excerpt** ile sonuç +### Yardımcı ve Uyumluluk Araçları + * `check_government_servers_health()`: Yargı kaynaklarının erişilebilirliğini kontrol eder. + * `search(query)`: ChatGPT Deep Research uyumluluğu için Bedesten destekli kaynaklarda arama yapar. + * `fetch(id)`: ChatGPT Deep Research uyumluluğu için tek bir Bedesten belge ID'sinin tam metnini getirir. +
--- @@ -387,7 +388,7 @@ Bu FastMCP sunucusu **24 temel MCP aracı** + **1 opsiyonel semantik arama arac **GENEL İSTATİSTİKLER:** - **Toplam Mahkeme/Kurum:** 15 farklı hukuki kurum (GİB Özelgeleri ve Sigorta Tahkim Komisyonu dahil) -- **Toplam MCP Tool:** 24 temel araç + 1 opsiyonel semantik arama aracı +- **Toplam MCP Tool:** 26 aktif araç + 1 opsiyonel semantik arama aracı - **Daire/Kurul Filtreleme:** 87 farklı seçenek (52 Yargıtay + 27 Danıştay + 8 Sayıştay) - **Tarih Filtreleme:** Birleşik Bedesten API aracında ISO 8601 formatında tam tarih aralığı desteği - **Kesin Cümle Arama:** Birleşik Bedesten API aracında çift tırnak ile tam cümle arama (`"\"mülkiyet kararı\""` formatı) diff --git a/mcp_server_main.py b/mcp_server_main.py index 5022aff..1c5762c 100644 --- a/mcp_server_main.py +++ b/mcp_server_main.py @@ -818,7 +818,10 @@ async def get_uyusmazlik_document_markdown_from_url( # --- Unified MCP Tools for Anayasa Mahkemesi --- @app.tool( - description="Use this when searching Turkish Constitutional Court decisions. Supports both norm control (legislation review) and individual applications (rights violations).", + description=( + "Use this when searching Turkish Constitutional Court decision records. Supports norm control decisions " + "and individual application decisions. Norm control filters include reviewed norm metadata; results are court decisions." + ), annotations={ "readOnlyHint": True, "openWorldHint": True, @@ -1095,7 +1098,11 @@ async def get_rekabet_kurumu_document( # --- MCP Tools for Bedesten (Unified Search Across All Courts) --- @app.tool( - description="Use this when searching across multiple Turkish courts in a single query. Supports Yargıtay, Danıştay, Local Courts, Appeals Courts, and KYB.", + description=( + "Use this for Turkish court decision records from Yargıtay, Danıştay, Local Courts, Appeals Courts, and KYB via Bedesten. " + "Prefer narrow court_types over all courts. pageSize is intentionally fixed to 10 results per page. " + "Bedesten is upstream rate-limited; avoid parallel repeated calls and wait retry_after seconds after 429 responses." + ), annotations={ "readOnlyHint": True, "openWorldHint": True, @@ -1119,7 +1126,7 @@ For best results, use exact phrases with quotes for legal terms."""), description="Court types: YARGITAYKARARI, DANISTAYKARAR, YERELHUKUK, ISTINAFHUKUK, KYB" ), # pageSize: int = Field(10, ge=1, le=10, description="Results per page (1-10)"), - pageNumber: int = Field(1, ge=1, description="Page number"), + pageNumber: int = Field(1, ge=1, description="Page number. Each page returns 10 results; pageSize is fixed by the server."), birimAdi: BirimAdiEnum = Field("ALL", description=""" Chamber filter (optional). Abbreviated values with Turkish names: • Yargıtay: H1-H23 (1-23. Hukuk Dairesi), C1-C23 (1-23. Ceza Dairesi), HGK (Hukuk Genel Kurulu), CGK (Ceza Genel Kurulu), BGK (Büyük Genel Kurulu), HBK (Hukuk Daireleri Başkanlar Kurulu), CBK (Ceza Daireleri Başkanlar Kurulu) @@ -1231,7 +1238,10 @@ For best results, use exact phrases with quotes for legal terms."""), raise @app.tool( - description="Use this when retrieving full text of any Bedesten-supported court decision. Returns clean Markdown format.", + description=( + "Use this when retrieving full text of a Bedesten search result by documentId. " + "Counts against the same Bedesten upstream rate limit as search; after 429, wait retry_after seconds before retrying." + ), annotations={ "readOnlyHint": True, "idempotentHint": True @@ -2187,7 +2197,11 @@ async def get_bddk_document_markdown( # --- MCP Tools for GİB (Gelir İdaresi Başkanlığı / Revenue Administration) Özelgeler --- @app.tool( - description="Search Turkish GİB özelgeler (Revenue Administration tax rulings) - 18k+ rulings on VAT, income tax, corporate tax, stamp duty interpretations. Supports keyword, date range, and law number filtering.", + description=( + "Search Turkish GİB özelge records (Revenue Administration tax rulings) - 18k+ rulings on VAT, " + "income tax, corporate tax, stamp duty interpretations. kanunNo filters the related law number; " + "returned documents are özelge/tax ruling records." + ), annotations={ "readOnlyHint": True, "openWorldHint": True, @@ -2197,7 +2211,7 @@ async def get_bddk_document_markdown( async def search_gib_ozelge( keywords: str = Field("", description="Turkish keywords searched in title, kanunNo and description (e.g., 'KDV oranı', 'kurumlar vergisi istisna')"), ozelgeNo: str = Field("", description="Exact özelge reference number (e.g., 'E-40247694-130-15524')"), - kanunNo: str = Field("", description="Law number filter, e.g. '3065' for KDV, '193' for Gelir Vergisi"), + kanunNo: str = Field("", description="Related law number filter for özelge records, e.g. '3065' for KDV, '193' for Gelir Vergisi"), ozelgeStartDate: str = Field("", description="Start date YYYY-MM-DD (e.g., '2024-01-01') or full ISO 8601"), ozelgeEndDate: str = Field("", description="End date YYYY-MM-DD (e.g., '2024-12-31') or full ISO 8601"), page: int = Field(1, ge=1, description="Page number (1-indexed)"), @@ -2429,54 +2443,42 @@ async def search_within_sigorta_tahkim_issue( # --- ChatGPT Deep Research Compatible Tools --- -def get_preview_text(markdown_content: str, skip_chars: int = 100, preview_chars: int = 200) -> str: - """ - Extract a preview of document text by skipping headers and showing meaningful content. - - Args: - markdown_content: Full document content in markdown format - skip_chars: Number of characters to skip from the beginning (default: 100) - preview_chars: Number of characters to show in preview (default: 200) - - Returns: - Preview text suitable for ChatGPT Deep Research - """ - if not markdown_content: - return "" - - # Remove common markdown artifacts and clean up - cleaned_content = markdown_content.strip() - - # Skip the first N characters (usually headers, metadata) - if len(cleaned_content) > skip_chars: - content_start = cleaned_content[skip_chars:] - else: - content_start = cleaned_content - - # Get the next N characters for preview - if len(content_start) > preview_chars: - preview = content_start[:preview_chars] - else: - preview = content_start - - # Clean up the preview - remove incomplete sentences at the end - preview = preview.strip() - - # If preview ends mid-sentence, try to end at last complete sentence - if preview and not preview.endswith('.'): - last_period = preview.rfind('.') - if last_period > 50: # Only if there's a reasonable sentence - preview = preview[:last_period + 1] - - # Add ellipsis if content was truncated - if len(content_start) > preview_chars: - preview += "..." - - return preview.strip() +def build_bedesten_title(decision: Any, court_name: str) -> str: + """Build a compact title from Bedesten search metadata without fetching the document.""" + title_parts = [court_name] + if getattr(decision, "birimAdi", None): + title_parts.append(str(decision.birimAdi)) + if getattr(decision, "esasNo", None): + title_parts.append(f"Esas: {decision.esasNo}") + if getattr(decision, "kararNo", None): + title_parts.append(f"Karar: {decision.kararNo}") + if getattr(decision, "kararTarihiStr", None): + title_parts.append(f"Tarih: {decision.kararTarihiStr}") + return " - ".join(title_parts) if title_parts else f"{court_name} - Document {decision.documentId}" + + +def build_bedesten_metadata_preview(decision: Any, court_name: str) -> str: + """Return Deep Research preview text using only search-result metadata.""" + preview_parts = [f"Kaynak: {court_name}"] + if getattr(decision, "birimAdi", None): + preview_parts.append(f"Daire/Kurul: {decision.birimAdi}") + if getattr(decision, "esasNo", None): + preview_parts.append(f"Esas No: {decision.esasNo}") + if getattr(decision, "kararNo", None): + preview_parts.append(f"Karar No: {decision.kararNo}") + if getattr(decision, "kararTarihiStr", None): + preview_parts.append(f"Karar Tarihi: {decision.kararTarihiStr}") + preview_parts.append("Tam metin için fetch aracını bu sonucun id değeriyle çağırın.") + return ". ".join(preview_parts) @app.tool( - description="Only for ChatGPT Deep Research. Use this when searching across all Turkish legal databases in a single query. Returns results in ChatGPT Deep Research compatible format (id, title, text, url). Supports: +term (must have), -term (exclude), \"exact phrase\", term1 OR term2. Use Turkish keywords for best results.", + description=( + "Only for ChatGPT Deep Research. Searches Bedesten-supported Turkish court databases and returns " + "OpenAI Deep Research compatible results (id, title, text, url). For regular MCP use, prefer " + "search_bedesten_unified. This tool does not fetch document bodies during search so one query stays " + "within Bedesten upstream rate limits; call fetch only for selected result IDs." + ), annotations={ "readOnlyHint": True, "openWorldHint": True, @@ -2506,21 +2508,21 @@ async def search( try: # Search all court types via unified Bedesten API court_types = [ - ("YARGITAYKARARI", "Yargıtay", "yargitay_bedesten"), - ("DANISTAYKARAR", "Danıştay", "danistay_bedesten"), - ("YERELHUKUK", "Yerel Hukuk Mahkemesi", "yerel_hukuk_bedesten"), - ("ISTINAFHUKUK", "İstinaf Hukuk Mahkemesi", "istinaf_hukuk_bedesten"), - ("KYB", "Kanun Yararına Bozma", "kyb_bedesten") + ("YARGITAYKARARI", "Yargıtay"), + ("DANISTAYKARAR", "Danıştay"), + ("YERELHUKUK", "Yerel Hukuk Mahkemesi"), + ("ISTINAFHUKUK", "İstinaf Hukuk Mahkemesi"), + ("KYB", "Kanun Yararına Bozma") ] - for item_type, court_name, id_prefix in court_types: + for item_type, court_name in court_types: try: search_results = await bedesten_client_instance.search_documents( BedestenSearchRequest( data=BedestenSearchData( phrase=query, # Use query as-is to support both regular and exact phrase searches itemTypeList=[item_type], - pageSize=10, + pageSize=5, pageNumber=1 ) ) @@ -2531,49 +2533,15 @@ async def search( logger.warning(f"No data returned from Bedesten API for {court_name}") continue - # Add results from this court type (limit to top 5 per court) + # Add results from metadata only. Fetching every document preview + # would turn one Deep Research search into ~30 Bedesten requests. for decision in search_results.data.emsalKararList[:5]: - # For ChatGPT Deep Research, fetch document content for preview - try: - # Fetch document content for preview - doc = await bedesten_client_instance.get_document_as_markdown(decision.documentId) - - # Generate preview text (skip first 100 chars, show next 200) - preview_text = get_preview_text(doc.markdown_content, skip_chars=100, preview_chars=200) - - # Build title from metadata - title_parts = [] - if decision.birimAdi: - title_parts.append(decision.birimAdi) - if decision.esasNo: - title_parts.append(f"Esas: {decision.esasNo}") - if decision.kararNo: - title_parts.append(f"Karar: {decision.kararNo}") - if decision.kararTarihiStr: - title_parts.append(f"Tarih: {decision.kararTarihiStr}") - - if title_parts: - title = " - ".join(title_parts) - else: - title = f"{court_name} - Document {decision.documentId}" - - # Add to results in OpenAI format - results.append({ - "id": decision.documentId, - "title": title, - "text": preview_text, - "url": f"https://mevzuat.adalet.gov.tr/ictihat/{decision.documentId}" - }) - - except Exception as e: - logger.warning(f"Could not fetch preview for document {decision.documentId}: {e}") - # Add minimal result without preview - results.append({ - "id": decision.documentId, - "title": f"{court_name} - Document {decision.documentId}", - "text": "Document preview not available", - "url": f"https://mevzuat.adalet.gov.tr/ictihat/{decision.documentId}" - }) + results.append({ + "id": decision.documentId, + "title": build_bedesten_title(decision, court_name), + "text": build_bedesten_metadata_preview(decision, court_name), + "url": f"https://mevzuat.adalet.gov.tr/ictihat/{decision.documentId}" + }) if search_results.data: logger.info(f"Found {len(search_results.data.emsalKararList)} results from {court_name}") @@ -2592,7 +2560,7 @@ async def search( # Danıştay Official API - use search_danistay_by_keyword instead # Constitutional Court - use search_anayasa_norm_denetimi_decisions instead # Competition Authority - use search_rekabet_kurumu_decisions instead - # Public Procurement Authority - use search_kik_decisions instead + # Public Procurement Authority - use search_kik_v2_decisions instead # Court of Accounts - use search_sayistay_* tools instead # UYAP Emsal - use search_emsal_detailed_decisions instead # Jurisdictional Disputes Court - use search_uyusmazlik_decisions instead @@ -2629,7 +2597,11 @@ async def search( raise @app.tool( - description="Only for ChatGPT Deep Research. Use this when retrieving a Turkish legal document by ID. Returns full document in ChatGPT Deep Research compatible format (id, title, text, url, metadata).", + description=( + "Only for ChatGPT Deep Research. Retrieves one Turkish legal document by numeric Bedesten ID. " + "For regular MCP use, prefer get_bedesten_document_markdown. This performs one Bedesten document request " + "and avoids an extra metadata lookup to respect upstream rate limits." + ), annotations={ "readOnlyHint": True, "openWorldHint": False, # Retrieves specific documents, not exploring @@ -2664,41 +2636,13 @@ async def fetch( # Use the numeric ID directly with Bedesten API doc = await bedesten_client_instance.get_document_as_markdown(id) - # Try to get additional metadata by searching for this specific document title = f"Turkish Legal Document {id}" - try: - # Quick search to get metadata for better title - search_results = await bedesten_client_instance.search_documents( - BedestenSearchRequest( - data=BedestenSearchData( - phrase=id, # Search by document ID - itemTypeList=["YARGITAYKARARI", "DANISTAYKARAR", "YERELHUKUK", "ISTINAFHUKUK", "KYB"], - pageSize=1, - pageNumber=1 - ) - ) - ) - - if search_results.data and search_results.data.emsalKararList: - decision = search_results.data.emsalKararList[0] - if decision.documentId == id: - # Build a proper title from metadata - title_parts = [] - if decision.birimAdi: - title_parts.append(decision.birimAdi) - if decision.esasNo: - title_parts.append(f"Esas: {decision.esasNo}") - if decision.kararNo: - title_parts.append(f"Karar: {decision.kararNo}") - if decision.kararTarihiStr: - title_parts.append(f"Tarih: {decision.kararTarihiStr}") - - if title_parts: - title = " - ".join(title_parts) - else: - title = f"Turkish Legal Decision {id}" - except Exception as e: - logger.warning(f"Could not fetch metadata for document {id}: {e}") + if doc.markdown_content: + for line in doc.markdown_content.splitlines(): + cleaned_line = line.strip().lstrip("#").strip() + if cleaned_line: + title = cleaned_line[:160] + break return { "id": id, @@ -2711,7 +2655,8 @@ async def fetch( "source_url": doc.source_url, "mime_type": doc.mime_type, "api_source": "Bedesten Unified API", - "chatgpt_deep_research": True + "chatgpt_deep_research": True, + "rate_limit_optimized": True } } @@ -2775,4 +2720,4 @@ def main(): logger.info(f"{app.name} server has shut down.") if __name__ == "__main__": - main() \ No newline at end of file + main()