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()