diff --git a/Dockerfile b/Dockerfile index 6b9d0b5..489ece5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -# -------- BASE IMAGE (includes Chromium & deps) ---------------------------- -FROM mcr.microsoft.com/playwright/python:v1.53.0-noble +# -------- BASE IMAGE --------------------------------------------------------- +FROM python:3.12-slim # -------- Runtime setup ---------------------------------------------------- WORKDIR /app diff --git a/kik_mcp_module/client.py b/kik_mcp_module/client.py deleted file mode 100644 index 5a3d51a..0000000 --- a/kik_mcp_module/client.py +++ /dev/null @@ -1,1199 +0,0 @@ -# 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 io -import random - -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() - # Ultra stealth browser configuration - self.browser = await self.playwright_instance.chromium.launch( - headless=True, - args=[ - # Disable automation indicators - '--no-first-run', - '--no-default-browser-check', - '--disable-dev-shm-usage', - '--disable-extensions', - '--disable-gpu', - '--disable-default-apps', - '--disable-translate', - '--disable-blink-features=AutomationControlled', - '--disable-ipc-flooding-protection', - '--disable-renderer-backgrounding', - '--disable-backgrounding-occluded-windows', - '--disable-client-side-phishing-detection', - '--disable-sync', - '--disable-features=TranslateUI,BlinkGenPropertyTrees', - '--disable-component-extensions-with-background-pages', - '--no-sandbox', # Sometimes needed for headless - '--disable-web-security', - '--disable-features=VizDisplayCompositor', - # Language and locale - '--lang=tr-TR', - '--accept-lang=tr-TR,tr;q=0.9,en;q=0.8', - # Performance optimizations - '--memory-pressure-off', - '--max_old_space_size=4096', - ] - ) - 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.") - # Ultra realistic context configuration - 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/131.0.0.0 Safari/537.36", - viewport={'width': 1920, 'height': 1080}, - screen={'width': 1920, 'height': 1080}, - device_scale_factor=1.0, - is_mobile=False, - has_touch=False, - # Localization - locale='tr-TR', - timezone_id='Europe/Istanbul', - # Realistic browser features - java_script_enabled=True, - accept_downloads=True, - ignore_https_errors=True, - # Color scheme and media - color_scheme='light', - reduced_motion='no-preference', - forced_colors='none', - # Additional headers for realism - extra_http_headers={ - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': 'tr-TR,tr;q=0.9,en;q=0.8', - 'Cache-Control': 'max-age=0', - 'DNT': '1', - 'Upgrade-Insecure-Requests': '1', - 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', - 'Sec-Ch-Ua-Mobile': '?0', - 'Sec-Ch-Ua-Platform': '"Windows"', - 'Sec-Fetch-Dest': 'document', - 'Sec-Fetch-Mode': 'navigate', - 'Sec-Fetch-Site': 'none', - 'Sec-Fetch-User': '?1', - }, - # Permissions to appear realistic - permissions=['geolocation'], - geolocation={'latitude': 41.0082, 'longitude': 28.9784}, # Istanbul - ) - 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) - - # CRITICAL: Anti-detection JavaScript injection - await self._inject_stealth_scripts() - 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.") - - async def _inject_stealth_scripts(self): - """ - Inject comprehensive stealth JavaScript to evade bot detection. - Overrides navigator properties and other fingerprinting vectors. - """ - if not self.page: - logger.warning("Cannot inject stealth scripts: page is None") - return - - logger.debug("Injecting comprehensive stealth scripts...") - - stealth_script = ''' - // Override navigator.webdriver - Object.defineProperty(navigator, 'webdriver', { - get: () => undefined, - configurable: true - }); - - // Override navigator properties to appear more human - Object.defineProperty(navigator, 'languages', { - get: () => ['tr-TR', 'tr', 'en-US', 'en'], - configurable: true - }); - - Object.defineProperty(navigator, 'platform', { - get: () => 'Win32', - configurable: true - }); - - Object.defineProperty(navigator, 'vendor', { - get: () => 'Google Inc.', - configurable: true - }); - - Object.defineProperty(navigator, 'deviceMemory', { - get: () => 8, - configurable: true - }); - - Object.defineProperty(navigator, 'hardwareConcurrency', { - get: () => 8, - configurable: true - }); - - Object.defineProperty(navigator, 'maxTouchPoints', { - get: () => 0, - configurable: true - }); - - // Override plugins to appear realistic - Object.defineProperty(navigator, 'plugins', { - get: () => { - return [ - { - 0: {type: "application/x-google-chrome-pdf", suffixes: "pdf", description: "Portable Document Format", enabledPlugin: Plugin}, - description: "Portable Document Format", - filename: "internal-pdf-viewer", - length: 1, - name: "Chrome PDF Plugin" - }, - { - 0: {type: "application/pdf", suffixes: "pdf", description: "", enabledPlugin: Plugin}, - description: "", - filename: "mhjfbmdgcfjbbpaeojofohoefgiehjai", - length: 1, - name: "Chrome PDF Viewer" - } - ]; - }, - configurable: true - }); - - // Override permissions - const originalQuery = window.navigator.permissions.query; - window.navigator.permissions.query = (parameters) => ( - parameters.name === 'notifications' ? - Promise.resolve({ state: Notification.permission }) : - originalQuery(parameters) - ); - - // Override WebGL rendering context - const getParameter = WebGLRenderingContext.prototype.getParameter; - WebGLRenderingContext.prototype.getParameter = function(parameter) { - if (parameter === 37445) { // UNMASKED_VENDOR_WEBGL - return 'Intel Inc.'; - } - if (parameter === 37446) { // UNMASKED_RENDERER_WEBGL - return 'Intel(R) Iris(R) Plus Graphics 640'; - } - return getParameter(parameter); - }; - - // Override canvas fingerprinting - const toBlob = HTMLCanvasElement.prototype.toBlob; - const toDataURL = HTMLCanvasElement.prototype.toDataURL; - const getImageData = CanvasRenderingContext2D.prototype.getImageData; - - const noisify = (canvas, context) => { - const imageData = context.getImageData(0, 0, canvas.width, canvas.height); - for (let i = 0; i < imageData.data.length; i += 4) { - imageData.data[i] += Math.floor(Math.random() * 10) - 5; - imageData.data[i + 1] += Math.floor(Math.random() * 10) - 5; - imageData.data[i + 2] += Math.floor(Math.random() * 10) - 5; - } - context.putImageData(imageData, 0, 0); - }; - - Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { - value: function(callback, type, encoderOptions) { - noisify(this, this.getContext('2d')); - return toBlob.apply(this, arguments); - } - }); - - Object.defineProperty(HTMLCanvasElement.prototype, 'toDataURL', { - value: function(type, encoderOptions) { - noisify(this, this.getContext('2d')); - return toDataURL.apply(this, arguments); - } - }); - - // Override AudioContext for audio fingerprinting - const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - const originalAnalyser = audioCtx.createAnalyser; - audioCtx.createAnalyser = function() { - const analyser = originalAnalyser.apply(this, arguments); - const getFloatFrequencyData = analyser.getFloatFrequencyData; - analyser.getFloatFrequencyData = function(array) { - getFloatFrequencyData.apply(this, arguments); - for (let i = 0; i < array.length; i++) { - array[i] += Math.random() * 0.0001; - } - }; - return analyser; - }; - - // Override screen properties - Object.defineProperty(window.screen, 'colorDepth', { - get: () => 24, - configurable: true - }); - - Object.defineProperty(window.screen, 'pixelDepth', { - get: () => 24, - configurable: true - }); - - // Override timezone - Date.prototype.getTimezoneOffset = function() { - return -180; // UTC+3 (Istanbul) - }; - - // Override document.cookie to prevent tracking - const originalCookieDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie') || - Object.getOwnPropertyDescriptor(HTMLDocument.prototype, 'cookie'); - if (originalCookieDescriptor && originalCookieDescriptor.configurable) { - Object.defineProperty(document, 'cookie', { - get: function() { - return originalCookieDescriptor.get.call(this); - }, - set: function(val) { - console.log('Cookie set blocked:', val); - return originalCookieDescriptor.set.call(this, val); - }, - configurable: true - }); - } - - // Remove automation traces - delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array; - delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise; - delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol; - delete window.cdc_adoQpoasnfa76pfcZLmcfl_JSON; - delete window.cdc_adoQpoasnfa76pfcZLmcfl_Object; - delete window.cdc_adoQpoasnfa76pfcZLmcfl_Proxy; - - // Add realistic performance timing - if (window.performance && window.performance.timing) { - const timing = window.performance.timing; - const now = Date.now(); - Object.defineProperty(timing, 'navigationStart', { value: now - Math.floor(Math.random() * 1000) + 1000, configurable: false }); - Object.defineProperty(timing, 'loadEventEnd', { value: now - Math.floor(Math.random() * 100) + 100, configurable: false }); - } - - console.log('✓ Stealth scripts injected successfully'); - ''' - - try: - await self.page.add_init_script(stealth_script) - logger.debug("✅ Stealth scripts injected successfully") - except Exception as e: - logger.warning(f"⚠️ Failed to inject stealth scripts: {e}") - - async def _simulate_human_behavior(self, fast_mode: bool = True): - """ - Simulate realistic human behavior patterns to avoid detection. - Includes mouse movements, typing patterns, and natural delays. - - Args: - fast_mode: If True, use minimal timing for speed optimization - """ - if not self.page: - logger.warning("Cannot simulate human behavior: page is None") - return - - logger.debug("🤖 Simulating human behavior patterns...") - - try: - if fast_mode: - # ULTRA-FAST MODE: Minimal human behavior - viewport_size = self.page.viewport_size - if viewport_size and random.random() < 0.7: # 70% chance to do movement - width, height = viewport_size['width'], viewport_size['height'] - - # Single quick mouse movement - x = random.randint(200, width - 200) - y = random.randint(200, height - 200) - await self.page.mouse.move(x, y) - - # Brief scroll (50% chance) - if random.random() < 0.5: - await self.page.mouse.wheel(0, random.randint(50, 100)) - - # Ultra-minimal delay - await asyncio.sleep(random.uniform(0.05, 0.15)) # Reduced from 0.1-0.3 - - else: - # FULL MODE: Original comprehensive behavior - viewport_size = self.page.viewport_size - if viewport_size: - width, height = viewport_size['width'], viewport_size['height'] - - # Generate 3-5 random mouse movements - movements = random.randint(3, 5) - logger.debug(f" 🖱️ Performing {movements} random mouse movements") - - for i in range(movements): - x = random.randint(100, width - 100) - y = random.randint(100, height - 100) - - # Move mouse with realistic speed (not instant) - await self.page.mouse.move(x, y) - await asyncio.sleep(random.uniform(0.1, 0.3)) - - # 2. Scroll simulation - logger.debug(" 📜 Simulating scroll behavior") - scroll_amount = random.randint(100, 300) - await self.page.mouse.wheel(0, scroll_amount) - await asyncio.sleep(random.uniform(0.2, 0.5)) - - # Scroll back up - await self.page.mouse.wheel(0, -scroll_amount) - await asyncio.sleep(random.uniform(0.2, 0.4)) - - # 3. Random page interaction delays - await asyncio.sleep(random.uniform(0.5, 1.5)) - - logger.debug("✅ Human behavior simulation completed") - - except Exception as e: - logger.warning(f"⚠️ Human behavior simulation failed: {e}") - - async def _human_type(self, selector: str, text: str, clear_first: bool = True, fast_mode: bool = True): - """ - Type text with human-like patterns and delays. - - Args: - selector: CSS selector for the input element - text: Text to type - clear_first: Whether to clear the field first - fast_mode: If True, use minimal delays for speed optimization - """ - if not self.page: - logger.warning("Cannot perform human typing: page is None") - return - - try: - if fast_mode: - # FAST MODE: Direct fill for speed - await self.page.fill(selector, text) - await asyncio.sleep(random.uniform(0.02, 0.05)) # Reduced from 0.05-0.1 - else: - # FULL MODE: Character-by-character human typing - # Focus on the element first - await self.page.focus(selector) - await asyncio.sleep(random.uniform(0.1, 0.3)) - - # Clear field if requested - if clear_first: - await self.page.keyboard.press('Control+a') - await asyncio.sleep(random.uniform(0.05, 0.15)) - await self.page.keyboard.press('Delete') - await asyncio.sleep(random.uniform(0.05, 0.15)) - - # Type each character with human-like delays - for char in text: - await self.page.keyboard.type(char) - # Human typing speed: 50-150ms between characters - delay = random.uniform(0.05, 0.15) - - # Occasional longer pauses (thinking) - if random.random() < 0.1: # 10% chance - delay += random.uniform(0.2, 0.8) - - await asyncio.sleep(delay) - - # Brief pause after typing - await asyncio.sleep(random.uniform(0.2, 0.6)) - - logger.debug(f"✅ Human-typed '{text}' into {selector}") - - except Exception as e: - logger.warning(f"⚠️ Human typing failed: {e}") - - async def _human_click(self, selector: str, wait_before: bool = True, wait_after: bool = True, fast_mode: bool = True): - """ - Perform a human-like click with realistic delays and mouse movement. - - Args: - selector: CSS selector or element to click - wait_before: Whether to wait before clicking - wait_after: Whether to wait after clicking - fast_mode: If True, use minimal delays for speed optimization - """ - if not self.page: - logger.warning("Cannot perform human click: page is None") - return - - try: - if fast_mode: - # FAST MODE: Direct click with minimal delay - if wait_before: - await asyncio.sleep(random.uniform(0.02, 0.08)) # Reduced from 0.05-0.15 - - await self.page.click(selector) - - if wait_after: - await asyncio.sleep(random.uniform(0.02, 0.08)) # Reduced from 0.05-0.15 - - else: - # FULL MODE: Realistic mouse movement and timing - # Wait before clicking (thinking time) - if wait_before: - await asyncio.sleep(random.uniform(0.3, 0.8)) - - # Get element bounds for realistic mouse movement - element = await self.page.query_selector(selector) - if element: - box = await element.bounding_box() - if box: - # Move to element with slight randomness - center_x = box['x'] + box['width'] / 2 - center_y = box['y'] + box['height'] / 2 - - # Add small random offset - offset_x = random.uniform(-10, 10) - offset_y = random.uniform(-5, 5) - - await self.page.mouse.move(center_x + offset_x, center_y + offset_y) - await asyncio.sleep(random.uniform(0.1, 0.3)) - - # Perform click - await self.page.mouse.click(center_x + offset_x, center_y + offset_y) - - logger.debug(f"✅ Human-clicked {selector}") - else: - # Fallback to regular click - await self.page.click(selector) - logger.debug(f"✅ Fallback-clicked {selector}") - else: - logger.warning(f"⚠️ Element not found for human click: {selector}") - return - - # Wait after clicking (processing time) - if wait_after: - await asyncio.sleep(random.uniform(0.2, 0.6)) - - logger.debug(f"✅ Human-clicked {selector}") - - except Exception as e: - logger.warning(f"⚠️ Human click failed: {e}") - - async def _simulate_page_exploration(self, fast_mode: bool = True): - """ - Simulate natural page exploration before performing the main task. - This helps establish a more human-like session. - - Args: - fast_mode: If True, use minimal exploration for speed optimization - """ - if not self.page: - return - - logger.debug("🕵️ Simulating page exploration...") - - try: - if fast_mode: - # ULTRA-FAST MODE: Minimal exploration - await asyncio.sleep(random.uniform(0.05, 0.1)) # Reduced from 0.1-0.3 - - # Single mouse movement (optional) - try: - elements = await self.page.query_selector_all("input, button") - if elements and random.random() < 0.5: # 50% chance to skip - element = random.choice(elements) - box = await element.bounding_box() - if box: - center_x = box['x'] + box['width'] / 2 - center_y = box['y'] + box['height'] / 2 - await self.page.mouse.move(center_x, center_y) - except: - pass - - await asyncio.sleep(random.uniform(0.02, 0.05)) # Reduced from 0.05-0.15 - - else: - # FULL MODE: Comprehensive exploration - # 1. Brief pause to "read" the page - await asyncio.sleep(random.uniform(1.0, 2.5)) - - # 2. Move mouse to various UI elements (like a human would explore) - explore_selectors = [ - "h1", "h2", ".navbar", "#header", ".logo", - "input", "button", "a", ".form-group" - ] - - explored = 0 - for selector in explore_selectors: - elements = await self.page.query_selector_all(selector) - if elements and explored < 3: # Explore max 3 elements - element = random.choice(elements) - box = await element.bounding_box() - if box: - center_x = box['x'] + box['width'] / 2 - center_y = box['y'] + box['height'] / 2 - - await self.page.mouse.move(center_x, center_y) - await asyncio.sleep(random.uniform(0.3, 0.8)) - explored += 1 - - # 3. Small scroll to simulate reading - await self.page.mouse.wheel(0, random.randint(50, 150)) - await asyncio.sleep(random.uniform(0.5, 1.2)) - - logger.debug("✅ Page exploration completed") - - except Exception as e: - logger.debug(f"⚠️ Page exploration failed: {e}") - - 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}) - - logger.debug(f"Looking for table with ID: {self.RESULTS_TABLE_ID}") - if not table: - logger.warning(f"Table with ID '{self.RESULTS_TABLE_ID}' not found in HTML") - # Log available tables for debugging - all_tables = soup.find_all("table") - logger.debug(f"Found {len(all_tables)} tables in HTML") - for idx, tbl in enumerate(all_tables): - table_id = tbl.get('id', 'no-id') - table_class = tbl.get('class', 'no-class') - rows = tbl.find_all('tr') - logger.debug(f"Table {idx}: id='{table_id}', class='{table_class}', rows={len(rows)}") - - # If this looks like a results table, try to use it - if (table_id and ('grd' in table_id.lower() or 'kurul' in table_id.lower() or 'sonuc' in table_id.lower())) or \ - (isinstance(table_class, list) and any('grid' in cls.lower() or 'result' in cls.lower() for cls in table_class)) or \ - len(rows) > 3: # Table with multiple rows might be results - logger.info(f"Trying to parse table {idx} as potential results table: id='{table_id}'") - table = tbl - break - - if not table: - logger.error("No suitable results table found") - return entries - - rows = table.find_all("tr") - logger.info(f"Found {len(rows)} rows in results table") - - # Debug: Log first few rows structure - for i, row in enumerate(rows[:3]): - cells = row.find_all(["td", "th"]) - cell_texts = [cell.get_text(strip=True)[:30] for cell in cells] - logger.info(f"Row {i} structure: {len(cells)} cells: {cell_texts}") - - for row_idx, row in enumerate(rows): - # Skip first row (search bar with colspan=7) and second row (header with 6 cells) - if row_idx < 2: - logger.debug(f"Skipping header row {row_idx}") - continue - - cells = row.find_all("td") - logger.debug(f"Row {row_idx}: Found {len(cells)} cells") - - # Log cell contents for debugging - if cells and row_idx < 5: # Log first few data rows - for cell_idx, cell in enumerate(cells): - cell_text = cell.get_text(strip=True)[:50] # First 50 chars - logger.debug(f" Cell {cell_idx}: '{cell_text}...'") - - # Be more flexible with cell count - try 6 cells first, then adapt - if len(cells) >= 5: # At least 5 cells for minimum required data - try: - # Try to find preview button in first cell or any cell with a link - preview_button_tag = None - event_target = "" - - # Look for preview button in first few cells - for cell_idx in range(min(3, len(cells))): - cell = cells[cell_idx] - # Try multiple patterns for preview button (based on actual HTML structure) - preview_candidates = [ - cell.find("a", id="btnOnizle"), # Exact match - cell.find("a", id=re.compile(r"btnOnizle$")), - cell.find("a", id=re.compile(r"btn.*Onizle")), - cell.find("a", id=re.compile(r".*Onizle.*")), - cell.find("a", href=re.compile(r"__doPostBack")) - ] - - for candidate in preview_candidates: - if candidate and candidate.has_attr('href'): - match = re.search(r"__doPostBack\('([^']*)','([^']*)'\)", candidate['href']) - if match: - event_target = match.group(1) - preview_button_tag = candidate - logger.debug(f"Row {row_idx}: Found event_target '{event_target}' in cell {cell_idx}") - break - - if preview_button_tag: - break - - if not preview_button_tag: - logger.debug(f"Row {row_idx}: No preview button found in any cell") - # Log what links we found - for cell_idx, cell in enumerate(cells[:3]): - links_in_cell = cell.find_all("a") - logger.debug(f" Cell {cell_idx}: {len(links_in_cell)} links") - for link in links_in_cell[:2]: - logger.debug(f" Link id='{link.get('id')}', href='{link.get('href', '')[:50]}...'") - - # Try to find decision data spans with more flexible patterns - karar_no_span = None - karar_tarihi_span = None - idare_span = None - basvuru_sahibi_span = None - ihale_span = None - - # Try different span patterns for karar no (usually in cell 1) - for cell_idx in range(min(4, len(cells))): - if not karar_no_span: - cell = cells[cell_idx] - candidates = [ - cell.find("span", id="lblKno"), # Exact match based on actual HTML - cell.find("span", id=re.compile(r"lblKno$")), - cell.find("span", id=re.compile(r".*Kno.*")), - cell.find("span", id=re.compile(r".*KararNo.*")), - cell.find("span", id=re.compile(r".*No.*")) - ] - for candidate in candidates: - if candidate and candidate.get_text(strip=True): - karar_no_span = candidate - logger.debug(f"Row {row_idx}: Found karar_no in cell {cell_idx}") - break - - # Try different patterns for karar tarihi (usually in cell 2) - for cell_idx in range(min(4, len(cells))): - if not karar_tarihi_span: - cell = cells[cell_idx] - candidates = [ - cell.find("span", id="lblKtar"), # Exact match based on actual HTML - cell.find("span", id=re.compile(r"lblKtar$")), - cell.find("span", id=re.compile(r".*Ktar.*")), - cell.find("span", id=re.compile(r".*Tarih.*")), - cell.find("span", id=re.compile(r".*Date.*")) - ] - for candidate in candidates: - if candidate and candidate.get_text(strip=True): - # Check if it looks like a date - text = candidate.get_text(strip=True) - if re.match(r'\d{1,2}[./]\d{1,2}[./]\d{4}', text): - karar_tarihi_span = candidate - logger.debug(f"Row {row_idx}: Found karar_tarihi in cell {cell_idx}") - break - - # Find other spans in remaining cells (if we have 6 cells) - using exact IDs - if len(cells) >= 6: - idare_span = cells[3].find("span", id="lblIdare") or cells[3].find("span") - basvuru_sahibi_span = cells[4].find("span", id="lblSikayetci") or cells[4].find("span") - ihale_span = cells[5].find("span", id="lblIhale") or cells[5].find("span") - elif len(cells) == 5: - # Adjust for 5-cell layout - idare_span = cells[2].find("span") if cells[2] != cells[1] else None - basvuru_sahibi_span = cells[3].find("span") if len(cells) > 3 else None - ihale_span = cells[4].find("span") if len(cells) > 4 else None - - # Log what we found - logger.debug(f"Row {row_idx}: karar_no_span={karar_no_span is not None}, " - f"karar_tarihi_span={karar_tarihi_span is not None}, " - f"event_target={bool(event_target)}") - - # For KIK, we need at least karar_no and karar_tarihi, event_target is helpful but not critical - if not (karar_no_span and karar_tarihi_span): - logger.debug(f"Row {row_idx}: Missing required fields (karar_no or karar_tarihi), skipping") - # Log what spans we found in cells - for i, cell in enumerate(cells): - spans = cell.find_all("span") - if spans: - span_info = [] - for s in spans: - span_id = s.get('id', 'no-id') - span_text = s.get_text(strip=True)[:20] - span_info.append(f"{span_id}:'{span_text}...'") - logger.debug(f" Cell {i} spans: {span_info}") - continue - - # If we don't have event_target, we can still create an entry but mark it specially - if not event_target: - logger.warning(f"Row {row_idx}: No event_target found, document retrieval won't work") - event_target = f"missing_target_row_{row_idx}" # Placeholder - - # Karar tipini arama parametresinden alıyoruz, çünkü HTML'de direkt olarak bulunmuyor. - try: - 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) - logger.info(f"Row {row_idx}: Successfully parsed decision: {entry.karar_no_str}") - except Exception as e: - logger.error(f"Row {row_idx}: Error creating KikDecisionEntry: {e}") - continue - - except Exception as e: - logger.error(f"Error parsing row {row_idx}: {e}", exc_info=True) - else: - logger.warning(f"Row {row_idx}: Expected at least 5 cells but found {len(cells)}, skipping") - if len(cells) > 0: - cell_texts = [cell.get_text(strip=True)[:50] for cell in cells[:3]] - logger.debug(f"Row {row_idx} cells preview: {cell_texts}") - - logger.info(f"Parsed {len(entries)} decision entries from {len(rows)} rows") - 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) - - # Simulate natural page exploration after navigation (FAST MODE) - await self._simulate_page_exploration(fast_mode=True) - - 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}" - logger.info(f"Selecting radio button: {js_target_radio}") - async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout): - await page.evaluate(f"javascript:__doPostBack('{js_target_radio}','')") - # Ultra-fast wait for page to stabilize after radio button change - await page.wait_for_timeout(300) # Reduced from 1000ms - logger.info("Radio button selection completed") - - # Helper function for human-like form filling (FAST MODE) - async def human_fill_if_value(selector_key: str, value: Optional[str]): - if value is not None: - selector = self.FIELD_LOCATORS[selector_key] - await self._human_type(selector, value, fast_mode=True) - - # 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.") - - # Fill form fields with FAST human-like behavior - logger.info("Filling form fields with fast mode...") - - # Start with FAST mouse behavior simulation - await self._simulate_human_behavior(fast_mode=True) - - await human_fill_if_value('karar_metni', search_params.karar_metni) - await human_fill_if_value('karar_no', karar_no_for_kik_form) # Dönüştürülmüş halini kullan - await human_fill_if_value('karar_tarihi_baslangic', search_params.karar_tarihi_baslangic) - await human_fill_if_value('karar_tarihi_bitis', search_params.karar_tarihi_bitis) - await human_fill_if_value('resmi_gazete_sayisi', search_params.resmi_gazete_sayisi) - await human_fill_if_value('resmi_gazete_tarihi', search_params.resmi_gazete_tarihi) - await human_fill_if_value('basvuru_konusu_ihale', search_params.basvuru_konusu_ihale) - await human_fill_if_value('basvuru_sahibi', search_params.basvuru_sahibi) - await human_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) - await page.wait_for_timeout(50) # Reduced from 100ms - - logger.info("Form filling completed, preparing for search...") - - # Additional FAST human behavior before search - await self._simulate_human_behavior(fast_mode=True) - - action_is_search_button_click = (search_params.page == 1) - event_target_for_submit: str - - try: - if action_is_search_button_click: - event_target_for_submit = self.FIELD_LOCATORS['search_button_id'] - # Use human-like clicking for search button - search_button_selector = f"a[id='{event_target_for_submit}']" - logger.info(f"Performing human-like search button click...") - - try: - # Hide datepicker first to prevent interference - await page.evaluate("$('#ui-datepicker-div').hide()") - - # FAST Human-like click on search button - await self._human_click(search_button_selector, wait_before=True, wait_after=False, fast_mode=True) - - # Wait for navigation - await page.wait_for_load_state("networkidle", timeout=self.request_timeout) - logger.info("Search navigation completed successfully") - except Exception as e: - logger.warning(f"Human click failed, falling back to JavaScript: {e}") - # Hide datepicker and use JavaScript fallback - await page.evaluate("$('#ui-datepicker-div').hide()") - async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout): - await page.evaluate(f"javascript:__doPostBack('{event_target_for_submit}','')") - logger.info("Search navigation completed via fallback") - else: - # Pagination - use original method for consistency - page_link_ctl_number = search_params.page + 2 - event_target_for_submit = f"ctl00$ContentPlaceHolder1$grdKurulKararSorguSonuc$ctl14$ctl{page_link_ctl_number:02d}" - logger.info(f"Executing pagination with event target: {event_target_for_submit}") - - async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout): - await page.evaluate(f"javascript:__doPostBack('{event_target_for_submit}','')") - logger.info("Pagination navigation completed successfully") - except PlaywrightTimeoutError: - logger.warning("Search navigation timed out, but continuing...") - await page.wait_for_timeout(5000) # Longer wait if navigation fails - - # Ultra-fast wait time for results to load - logger.info("Waiting for search results to load...") - await page.wait_for_timeout(500) # Reduced from 1000ms - - results_table_dom_selector = f"table#{self.RESULTS_TABLE_ID}" - try: - # First wait for any tables to appear (more general check) - logger.info("Waiting for any tables to appear...") - await page.wait_for_function(""" - () => document.querySelectorAll('table').length > 0 - """, timeout=4000) # Reduced from 8000ms - logger.info("At least one table appeared") - - # Then wait for our specific table - await page.wait_for_selector(results_table_dom_selector, timeout=4000, state="attached") # Reduced from 8000ms - logger.debug("Results table attached to DOM") - - # Wait for table to have some content (more than just headers) - await page.wait_for_function(f""" - () => {{ - const table = document.querySelector('{results_table_dom_selector}'); - return table && table.querySelectorAll('tr').length > 2; - }} - """, timeout=4000) # Reduced from 20000ms - logger.debug("Results table populated with data") - - # Ultra-fast additional wait for any remaining JavaScript - await page.wait_for_timeout(500) # Reduced from 3000ms - - except PlaywrightTimeoutError: - logger.warning(f"Timeout waiting for results table '{results_table_dom_selector}'.") - # Try one more wait for content placeholder - try: - await page.wait_for_selector("#ctl00_ContentPlaceHolder1", timeout=10000) - logger.info("ContentPlaceHolder1 found, checking for tables...") - await page.wait_for_timeout(5000) - except PlaywrightTimeoutError: - logger.warning("ContentPlaceHolder1 also not found - content may not have loaded") - - 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 - try: - # Convert HTML string to bytes and create BytesIO stream - html_bytes = cleaned_html.encode('utf-8') - html_stream = io.BytesIO(html_bytes) - - # Pass BytesIO stream to MarkItDown to avoid temp file creation - md_converter = MarkItDown(enable_plugins=True, remove_alt_whitespace=True, keep_underline=True) - markdown_output = md_converter.convert(html_stream).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) - 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) diff --git a/kik_mcp_module/client_v2.py b/kik_mcp_module/client_v2.py index a72be57..62f5e8a 100644 --- a/kik_mcp_module/client_v2.py +++ b/kik_mcp_module/client_v2.py @@ -325,7 +325,7 @@ class KikV2ApiClient: This method uses a two-step process: 1. Call GetSorgulamaUrl endpoint to get the actual document URL - 2. Use Playwright to navigate to that URL and extract content + 2. Use httpx to fetch the document content Args: document_id: The gundemMaddesiId from search results diff --git a/kik_mcp_module/models.py b/kik_mcp_module/models.py deleted file mode 100644 index 6b6632b..0000000 --- a/kik_mcp_module/models.py +++ /dev/null @@ -1,74 +0,0 @@ -# kik_mcp_module/models.py -from pydantic import BaseModel, Field, HttpUrl, computed_field, ConfigDict -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") - karar_no: str = Field("", description="No") - karar_tarihi_baslangic: str = Field("", description="Start", pattern=r"^\d{2}\.\d{2}\.\d{4}$|^$") - karar_tarihi_bitis: str = Field("", description="End", pattern=r"^\d{2}\.\d{2}\.\d{4}$|^$") - resmi_gazete_sayisi: str = Field("", description="Gazette") - resmi_gazete_tarihi: str = Field("", description="Date", pattern=r"^\d{2}\.\d{2}\.\d{4}$|^$") - basvuru_konusu_ihale: str = Field("", description="Subject") - basvuru_sahibi: str = Field("", description="Applicant") - ihaleyi_yapan_idare: str = Field("", description="Entity") - yil: str = Field("", description="Year") - karar_metni: str = Field("", description="Text") - page: int = Field(1, ge=1, description="Page") - -class KikDecisionEntry(BaseModel): - """Represents a single decision entry from KIK search results.""" - preview_event_target: str = Field(..., description="Event target") - karar_no_str: str = Field(..., alias="kararNo", description="Decision number") - karar_tipi: KikKararTipi = Field(..., description="Decision type") - - karar_tarihi_str: str = Field(..., alias="kararTarihi", description="Date") - idare_str: str = Field("", alias="idare", description="Entity") - basvuru_sahibi_str: str = Field("", alias="basvuruSahibi", description="Applicant") - ihale_konusu_str: str = Field("", alias="ihaleKonusu", description="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') - - model_config = ConfigDict(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="Request ID") - retrieved_karar_no: Optional[str] = Field(None, description="Decision number") - retrieved_karar_tipi: Optional[KikKararTipi] = Field(None, description="Decision type") - - karar_id_param_from_url: Optional[str] = Field(None, alias="kararIdParam", description="Internal ID") - markdown_chunk: Optional[str] = Field(None, description="Content") - source_url: Optional[str] = Field(None, description="Source URL") - error_message: Optional[str] = Field(None, description="Error") - current_page: int = Field(1, description="Page") - total_pages: int = Field(1, description="Total pages") - is_paginated: bool = Field(False, description="Paginated") - full_content_char_count: Optional[int] = Field(None, description="Char count") - - model_config = ConfigDict(populate_by_name=True) \ No newline at end of file diff --git a/mcp_server_main.py b/mcp_server_main.py index 35f2025..42b23fa 100644 --- a/mcp_server_main.py +++ b/mcp_server_main.py @@ -2191,47 +2191,14 @@ async def fetch( # --- Token Metrics Tool Removed for Optimization --- -def ensure_playwright_browsers(): - """Ensure Playwright browsers are installed for KIK tool functionality.""" - try: - import subprocess - import os - - # Check if chromium is already installed - chromium_path = os.path.expanduser("~/Library/Caches/ms-playwright/chromium-1179") - if os.path.exists(chromium_path): - logger.info("Playwright Chromium browser already installed.") - return - - logger.info("Installing Playwright Chromium browser for KIK tool...") - result = subprocess.run( - ["python", "-m", "playwright", "install", "chromium"], - capture_output=True, - text=True, - timeout=300 # 5 minutes timeout - ) - - if result.returncode == 0: - logger.info("Playwright Chromium browser installed successfully.") - else: - logger.warning(f"Failed to install Playwright browser: {result.stderr}") - logger.warning("KIK tool may not work properly without Playwright browsers.") - - except Exception as e: - logger.warning(f"Could not auto-install Playwright browsers: {e}") - logger.warning("KIK tool may not work properly. Manual installation: 'playwright install chromium'") - def main(): # Initialize the app properly with create_app() global app app = create_app() - + logger.info(f"Starting {app.name} server via main() function...") # logger.info(f"Logs will be written to: {LOG_FILE_PATH}") # File logging disabled - - # Ensure Playwright browsers are installed - ensure_playwright_browsers() - + try: app.run() except KeyboardInterrupt: diff --git a/postinstall.sh b/postinstall.sh deleted file mode 100755 index 90c9111..0000000 --- a/postinstall.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -# Post-install script for FastMCP Cloud -# Installs Chromium browser for Playwright - -echo "Installing Playwright Chromium browser..." -playwright install chromium -echo "Chromium installation complete." diff --git a/pyproject.toml b/pyproject.toml index 1493c4b..7596c13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ dependencies = [ "markitdown[pdf]>=0.1.1", "pydantic>=2.11.4", "aiohttp>=3.11.18", - "playwright>=1.52.0", "fastmcp>=2.10.5", "pypdf>=5.5.0", "fastapi>=0.115.14", diff --git a/requirements.txt.bak b/requirements.txt.bak deleted file mode 100644 index edfd70a..0000000 --- a/requirements.txt.bak +++ /dev/null @@ -1,11 +0,0 @@ -fastmcp -httpx -beautifulsoup4 -markitdown[pdf] -pydantic -aiohttp -playwright -pypdf -fastapi>=0.115.14 -uvicorn[standard]>=0.30.0 -starlette>=0.37.0 \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/.dockerignore b/saidsurucu-yargi-mcp-f5fa007/.dockerignore deleted file mode 100644 index e9f20bb..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/.dockerignore +++ /dev/null @@ -1,186 +0,0 @@ -# flyctl launch added from .gitignore -# Byte-compiled / optimized / DLL files -**/__pycache__ -**/*.py[cod] -**/*$py.class - -# C extensions -**/*.so - -# Distribution / packaging -**/.Python -**/build -**/develop-eggs -**/dist -**/downloads -**/eggs -**/.eggs -**/lib -**/lib64 -**/parts -**/sdist -**/var -**/wheels -**/share/python-wheels -**/*.egg-info -**/.installed.cfg -**/*.egg -**/MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -**/*.manifest -**/*.spec - -# Installer logs -**/pip-log.txt -**/pip-delete-this-directory.txt - -# Unit test / coverage reports -**/htmlcov -**/.tox -**/.nox -**/.coverage -**/.coverage.* -**/.cache -**/nosetests.xml -**/coverage.xml -**/*.cover -**/*.py,cover -**/.hypothesis -**/.pytest_cache -**/cover - -# Translations -**/*.mo -**/*.pot - -# Django stuff: -**/*.log -**/local_settings.py -**/db.sqlite3 -**/db.sqlite3-journal - -# Flask stuff: -**/instance -**/.webassets-cache - -# Scrapy stuff: -**/.scrapy - -# Sphinx documentation -**/docs/_build - -# PyBuilder -**/.pybuilder -**/target - -# Jupyter Notebook -**/.ipynb_checkpoints - -# IPython -**/profile_default -**/ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -**/.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -**/__pypackages__ - -# Celery stuff -**/celerybeat-schedule -**/celerybeat.pid - -# SageMath parsed files -**/*.sage.py - -# Environments -**/.env -**/.venv -**/env -**/venv -**/ENV -**/env.bak -**/venv.bak - -# Spyder project settings -**/.spyderproject -**/.spyproject - -# Rope project settings -**/.ropeproject - -# mkdocs documentation -site - -# mypy -**/.mypy_cache -**/.dmypy.json -**/dmypy.json - -# Pyre type checker -**/.pyre - -# pytype static type analyzer -**/.pytype - -# Cython debug symbols -**/cython_debug - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ -**/.DS_Store -**/hello.py - -**/*.html -**/fast-mcp-docs.md - -# Debug and test files -**/debug_* -**/test_* -**/CLAUDE.md - -# ASGI/Deployment files -**/ssl -**/*.pem -**/*.key -**/*.crt - -# Docker volumes -**/redis-data - -# Production logs -**/logs/*.log.* -**/Dockerfile -**/Dockerfile -fly.toml diff --git a/saidsurucu-yargi-mcp-f5fa007/.env.example b/saidsurucu-yargi-mcp-f5fa007/.env.example deleted file mode 100644 index 270a0fb..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/.env.example +++ /dev/null @@ -1,103 +0,0 @@ -# OAuth Configuration for Clerk + Google -# Copy this file to .env and fill in your actual values - -# ============================================================================= -# AUTHENTICATION SETTINGS -# ============================================================================= - -# Enable/disable authentication (set to "true" to enable OAuth) -ENABLE_AUTH=false - -# ============================================================================= -# CLERK CONFIGURATION -# ============================================================================= - -# Clerk API keys (get from https://dashboard.clerk.com/) -CLERK_SECRET_KEY=sk_test_your_secret_key_here -CLERK_PUBLISHABLE_KEY=pk_test_your_publishable_key_here - -# OAuth Redirect URLs -CLERK_OAUTH_REDIRECT_URL=http://localhost:8000/auth/callback -CLERK_FRONTEND_URL=http://localhost:3000 - -# Clerk domain issuer (usually auto-configured) -CLERK_ISSUER=https://your-clerk-domain.clerk.accounts.dev -CLERK_DOMAIN=your-clerk-domain - -# ============================================================================= -# GOOGLE OAUTH SETTINGS -# ============================================================================= -# Note: Google OAuth is configured through Clerk dashboard -# You need to: -# 1. Go to Clerk Dashboard > Social Connections -# 2. Enable Google provider -# 3. Add your Google OAuth client ID and secret -# 4. Configure redirect URIs in Google Console - -# ============================================================================= -# STRIPE CONFIGURATION (for payments/subscriptions) -# ============================================================================= - -STRIPE_SECRET=sk_test_your_stripe_secret_key_here -STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here - -# ============================================================================= -# SERVER CONFIGURATION -# ============================================================================= - -# CORS origins (comma-separated list) -ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8000,https://yourdomain.com - -# Server settings -HOST=0.0.0.0 -PORT=8000 -LOG_LEVEL=info - -# Base URL for the application (used for OAuth callbacks and API URLs) -BASE_URL=http://localhost:8000 - -# JWT Secret for MCP token generation -JWT_SECRET_KEY=your_jwt_secret_key_here - -# ============================================================================= -# MCP SERVER SETTINGS -# ============================================================================= - -# Additional MCP server configuration can go here -# For example, rate limiting, feature flags, etc. - -# Example: Rate limiting -# MAX_REQUESTS_PER_MINUTE=60 -# BURST_CAPACITY=20 - -# ============================================================================= -# USAGE INSTRUCTIONS -# ============================================================================= - -# 1. Copy this file to .env: -# cp .env.example .env - -# 2. Get Clerk credentials: -# - Sign up at https://clerk.com/ -# - Create a new application -# - Go to API Keys tab -# - Copy Secret Key and Publishable Key - -# 3. Configure Google OAuth in Clerk: -# - In Clerk Dashboard, go to Social Connections -# - Enable Google provider -# - Get Google OAuth credentials from Google Console -# - Add redirect URI: http://localhost:8000/auth/callback - -# 4. Update OAuth URLs: -# - Set CLERK_OAUTH_REDIRECT_URL to your callback URL -# - Set CLERK_FRONTEND_URL to your frontend application URL - -# 5. Enable authentication: -# - Set ENABLE_AUTH=true - -# 6. Test the OAuth flow: -# - Start server: uvicorn asgi_app:app --reload -# - Visit: http://localhost:8000/auth/login -# - Complete OAuth flow with Google -# - Check: http://localhost:8000/auth/user \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/.gitattributes b/saidsurucu-yargi-mcp-f5fa007/.gitattributes deleted file mode 100644 index dfe0770..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/saidsurucu-yargi-mcp-f5fa007/.github/workflows/publish.yml b/saidsurucu-yargi-mcp-f5fa007/.github/workflows/publish.yml deleted file mode 100644 index b1924b3..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/.github/workflows/publish.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Publish to PyPI - -on: - release: - types: [published] - workflow_dispatch: # Manual trigger for testing - -jobs: - pypi-publish: - name: Upload release to PyPI - runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/p/yargi-mcp - permissions: - id-token: write # IMPORTANT: this permission is mandatory for trusted publishing - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - - name: Build package - run: python -m build - - - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} - skip-existing: true \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/.gitignore b/saidsurucu-yargi-mcp-f5fa007/.gitignore deleted file mode 100644 index 7f3ac3d..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/.gitignore +++ /dev/null @@ -1,215 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ -.DS_Store -hello.py - -*.html -fast-mcp-docs.md - -# Debug and test files -debug_* -test_* -CLAUDE.md - -# ASGI/Deployment files -ssl/ -*.pem -*.key -*.crt - -# Docker volumes -redis-data/ - -# Production logs -logs/*.log.* - -# Remove these lines - we need deployment files in git: -# Dockerfile - NEEDED for SaaS deployment -# fly.toml - NEEDED for Fly.io deployment -# .github/workflows/fly-deploy.yml - NEEDED for GitHub Actions - -GEMINI.md -fly.toml -scripts/deploy-flyio.sh -docs/DEPLOYMENT_FLYIO.md -setup_jwt_template.py -mcp_server_main.py.backup -mcp_overhead_content.json -ANTHROPIC_TEST_README.md -extract_mcp_overhead.py -mcp_overhead_content.txt -mcp_overhead_summary.txt -run_http_server.py -run_local_test.py - -# MCP overhead analysis files -mcp_overhead_*.json -mcp_overhead_*.txt -mcp_test_results_*.json -mcp_quick_test_*.json - -# General text files (temporary notes, etc) -*.txt -analyze_playwright_mcp.py -measure_mcp_directly.py -playwright_mcp_overhead.json -simple_test.py -analyze_anayasa_html.py diff --git a/saidsurucu-yargi-mcp-f5fa007/5ire-settings.png b/saidsurucu-yargi-mcp-f5fa007/5ire-settings.png deleted file mode 100644 index 0fa460b..0000000 Binary files a/saidsurucu-yargi-mcp-f5fa007/5ire-settings.png and /dev/null differ diff --git a/saidsurucu-yargi-mcp-f5fa007/Dockerfile b/saidsurucu-yargi-mcp-f5fa007/Dockerfile deleted file mode 100644 index 111f909..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -# -------- BASE IMAGE (includes Chromium & deps) ---------------------------- -FROM mcr.microsoft.com/playwright/python:v1.53.0-noble - -# -------- Runtime setup ---------------------------------------------------- -WORKDIR /app - -# Copy dependency manifests first for layer-cache -COPY pyproject.toml poetry.lock* requirements*.txt* ./ - -# Fast, deterministic install with `uv` -RUN pip install --no-cache-dir uv && \ - uv pip install --system --no-cache-dir .[asgi,saas] - -# Copy application source -COPY . . - -# -------- Environment ------------------------------------------------------ -ENV PYTHONUNBUFFERED=1 -ENV ENABLE_AUTH=true -ENV PORT=8000 - -# -------- Health check ----------------------------------------------------- -HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ - CMD python -c "import httpx, os, sys; r=httpx.get(f'http://localhost:{os.getenv(\"PORT\",\"8000\")}/health'); sys.exit(0 if r.status_code==200 else 1)" - -EXPOSE 8000 - -# -------- Entrypoint ------------------------------------------------------- -CMD ["uvicorn", "asgi_app:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"] \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/LICENSE b/saidsurucu-yargi-mcp-f5fa007/LICENSE deleted file mode 100644 index cdf2fd2..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 saidsurucu - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/saidsurucu-yargi-mcp-f5fa007/Procfile b/saidsurucu-yargi-mcp-f5fa007/Procfile deleted file mode 100644 index 2b6b125..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: uvicorn asgi_app:app --host 0.0.0.0 --port $PORT \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/README.md b/saidsurucu-yargi-mcp-f5fa007/README.md deleted file mode 100644 index 570305d..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/README.md +++ /dev/null @@ -1,272 +0,0 @@ -# Yargı MCP: Türk Hukuk Kaynakları için MCP Sunucusu - -[![Star History Chart](https://api.star-history.com/svg?repos=saidsurucu/yargi-mcp&type=Date)](https://www.star-history.com/#saidsurucu/yargi-mcp&Date) - -Bu proje, çeşitli Türk hukuk kaynaklarına (Yargıtay, Danıştay, Emsal Kararlar, Uyuşmazlık Mahkemesi, Anayasa Mahkemesi - Norm Denetimi ile Bireysel Başvuru Kararları, Kamu İhale Kurulu Kararları, Rekabet Kurumu Kararları, Sayıştay Kararları, KVKK Kararları ve BDDK Kararları) erişimi kolaylaştıran bir [FastMCP](https://gofastmcp.com/) sunucusu oluşturur. Bu sayede, bu kaynaklardan veri arama ve belge getirme işlemleri, Model Context Protocol (MCP) destekleyen LLM (Büyük Dil Modeli) uygulamaları (örneğin Claude Desktop veya [5ire](https://5ire.app)) ve diğer istemciler tarafından araç (tool) olarak kullanılabilir hale gelir. - -![örnek](./ornek.png) - -🎯 **Temel Özellikler** - -🚀 **YÜKSEK PERFORMANS OPTİMİZASYONU:** Bu MCP sunucusu **%61.8 token azaltma** ile optimize edilmiştir (8,692 token tasarrufu). Claude AI ile daha hızlı yanıt süreleri ve daha verimli etkileşim sağlar. - -* Çeşitli Türk hukuk veritabanlarına programatik erişim için standart bir MCP arayüzü. -* **Kapsamlı Mahkeme Daire/Kurul Filtreleme:** 79 farklı daire/kurul filtreleme seçeneği -* **Dual/Triple API Desteği:** Her mahkeme için birden fazla API kaynağı ile maksimum kapsama -* **Kapsamlı Tarih Filtreleme:** Tüm Bedesten API araçlarında ISO 8601 formatında tarih aralığı filtreleme -* **Kesin Cümle Arama:** Tüm Bedesten API araçlarında çift tırnak ile tam cümle arama desteği -* Aşağıdaki kurumların kararlarını arama ve getirme yeteneği: - * **Yargıtay:** Detaylı kriterlerle karar arama ve karar metinlerini Markdown formatında getirme. **Dual API** (Ana + Bedesten) + **52 Daire/Kurul Filtreleme** + **Tarih & Kesin Cümle Arama** (Hukuk/Ceza Daireleri, Genel Kurullar) - * **Danıştay:** Anahtar kelime bazlı ve detaylı kriterlerle karar arama; karar metinlerini Markdown formatında getirme. **Triple API** (Keyword + Detailed + Bedesten) + **27 Daire/Kurul Filtreleme** + **Tarih & Kesin Cümle Arama** (İdari Daireler, Vergi/İdare Kurulları, Askeri Yüksek İdare Mahkemesi) - * **Yerel Hukuk Mahkemeleri:** Bedesten API ile yerel hukuk mahkemesi kararlarına erişim + **Tarih & Kesin Cümle Arama** - * **İstinaf Hukuk Mahkemeleri:** Bedesten API ile istinaf mahkemesi kararlarına erişim + **Tarih & Kesin Cümle Arama** - * **Kanun Yararına Bozma (KYB):** Bedesten API ile olağanüstü kanun yoluna erişim + **Tarih & Kesin Cümle Arama** - * **Emsal (UYAP):** Detaylı kriterlerle emsal karar arama ve karar metinlerini Markdown formatında getirme. - * **Uyuşmazlık Mahkemesi:** Form tabanlı kriterlerle karar arama ve karar metinlerini (URL ile erişilen) Markdown formatında getirme. - * **Anayasa Mahkemesi (Norm Denetimi):** Kapsamlı kriterlerle norm denetimi kararlarını arama; uzun karar metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme. - * **Anayasa Mahkemesi (Bireysel Başvuru):** Kapsamlı kriterlerle bireysel başvuru "Karar Arama Raporu" oluşturma ve listedeki kararların metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme. - * **KİK (Kamu İhale Kurulu):** Çeşitli kriterlerle Kurul kararlarını arama; uzun karar metinlerini (varsayılan 5.000 karakterlik) sayfalanmış Markdown formatında getirme. - * **Rekabet Kurumu:** Çeşitli kriterlerle Kurul kararlarını arama; karar metinlerini Markdown formatında getirme. - * **Sayıştay:** 3 karar türü ile kapsamlı denetim kararlarına erişim + **8 Daire Filtreleme** + **Tarih Aralığı & İçerik Arama** (Genel Kurul yorumlayıcı kararları, Temyiz Kurulu itiraz kararları, Daire ilk derece denetim kararları) - * **KVKK (Kişisel Verilerin Korunması Kurulu):** Brave Search API ile veri koruma kararlarını arama; uzun karar metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme + **Türkçe Arama** + **Site Hedeflemeli Arama** (kvkk.gov.tr kararları) - * **BDDK (Bankacılık Düzenleme ve Denetleme Kurumu):** Bankacılık düzenleme kararlarını arama; karar metinlerini Markdown formatında getirme + **Optimized Search** + **"Karar Sayısı" Targeting** + **Spesifik URL Filtreleme** (bddk.org.tr/Mevzuat/DokumanGetir) - -* Karar metinlerinin daha kolay işlenebilmesi için Markdown formatına çevrilmesi. -* Claude Desktop uygulaması ile `fastmcp install` komutu kullanılarak kolay entegrasyon. -* Yargı MCP artık [5ire](https://5ire.app) gibi Claude Desktop haricindeki MCP istemcilerini de destekliyor! ---- -
-🚀 Claude Haricindeki Modellerle Kullanmak İçin Çok Kolay Kurulum (Örnek: 5ire için) - -Bu bölüm, Yargı MCP aracını 5ire gibi Claude Desktop dışındaki MCP istemcileriyle kullanmak isteyenler içindir. - -* **Python Kurulumu:** Sisteminizde Python 3.11 veya üzeri kurulu olmalıdır. Kurulum sırasında "**Add Python to PATH**" (Python'ı PATH'e ekle) seçeneğini işaretlemeyi unutmayın. [Buradan](https://www.python.org/downloads/) indirebilirsiniz. -* **Git Kurulumu (Windows):** Bilgisayarınıza [git](https://git-scm.com/downloads/win) yazılımını indirip kurun. "Git for Windows/x64 Setup" seçeneğini indirmelisiniz. -* **`uv` Kurulumu:** - * **Windows Kullanıcıları (PowerShell):** Bir CMD ekranı açın ve bu kodu çalıştırın: `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"` - * **Mac/Linux Kullanıcıları (Terminal):** Bir Terminal ekranı açın ve bu kodu çalıştırın: `curl -LsSf https://astral.sh/uv/install.sh | sh` -* **Microsoft Visual C++ Redistributable (Windows):** Bazı Python paketlerinin doğru çalışması için gereklidir. [Buradan](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170) indirip kurun. -* İşletim sisteminize uygun [5ire](https://5ire.app) MCP istemcisini indirip kurun. -* 5ire'ı açın. **Workspace -> Providers** menüsünden kullanmak istediğiniz LLM servisinin API anahtarını girin. -* **Tools** menüsüne girin. **+Local** veya **New** yazan butona basın. - * **Tool Key:** `yargimcp` - * **Name:** `Yargı MCP` - * **Command:** - ``` - uvx yargi-mcp - ``` - * **Save** butonuna basarak kaydedin. -![5ire ayarları](./5ire-settings.png) -* Şimdi **Tools** altında **Yargı MCP**'yi görüyor olmalısınız. Üstüne geldiğinizde sağda çıkan butona tıklayıp etkinleştirin (yeşil ışık yanmalı). -* Artık Yargı MCP ile konuşabilirsiniz. - -
- ---- -
-⚙️ Claude Desktop Manuel Kurulumu - -1. **Ön Gereksinimler:** Python, `uv`, (Windows için) Microsoft Visual C++ Redistributable'ın sisteminizde kurulu olduğundan emin olun. Detaylı bilgi için yukarıdaki "5ire için Kurulum" bölümündeki ilgili adımlara bakabilirsiniz. -2. Claude Desktop **Settings -> Developer -> Edit Config**. -3. Açılan `claude_desktop_config.json` dosyasına `mcpServers` altına ekleyin: - - ```json - { - "mcpServers": { - // ... (varsa diğer sunucularınız) ... - "Yargı MCP": { - "command": "uvx", - "args": [ - "yargi-mcp" - ] - } - } - } - ``` -4. Claude Desktop'ı kapatıp yeniden başlatın. - -
- ---- -
-🌟 Gemini CLI ile Kullanım - -Yargı MCP'yi Gemini CLI ile kullanmak için: - -1. **Ön Gereksinimler:** Python, `uv`, (Windows için) Microsoft Visual C++ Redistributable'ın sisteminizde kurulu olduğundan emin olun. Detaylı bilgi için yukarıdaki "5ire için Kurulum" bölümündeki ilgili adımlara bakabilirsiniz. - -2. **Gemini CLI ayarlarını yapılandırın:** - - Gemini CLI'ın ayar dosyasını düzenleyin: - - **macOS/Linux:** `~/.gemini/settings.json` - - **Windows:** `%USERPROFILE%\.gemini\settings.json` - - Aşağıdaki `mcpServers` bloğunu ekleyin: - ```json - { - "theme": "Default", - "selectedAuthType": "###", - "mcpServers": { - "yargi_mcp": { - "command": "uvx", - "args": [ - "yargi-mcp" - ] - } - } - } - ``` - - **Yapılandırma açıklamaları:** - - `"yargi_mcp"`: Sunucunuz için yerel bir isim - - `"command"`: `uvx` komutu (uv'nin paket çalıştırma aracı) - - `"args"`: GitHub'dan doğrudan Yargı MCP'yi çalıştırmak için gerekli argümanlar - -3. **Kullanım:** - - Gemini CLI'ı başlatın - - Yargı MCP araçları otomatik olarak kullanılabilir olacaktır - - Örnek komutlar: - - "Yargıtay'ın mülkiyet hakkı ile ilgili son kararlarını ara" - - "Danıştay'ın imar planı iptaline ilişkin kararlarını bul" - - "Anayasa Mahkemesi'nin ifade özgürlüğü kararlarını getir" - -
- -
-🛠️ Kullanılabilir Araçlar (MCP Tools) - -Bu FastMCP sunucusu **19 optimize edilmiş MCP 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* - -### **Danıştay Araçları (Birleşik Bedesten API - Token Optimized)** -*Not: Danıştay araçları token verimliliği için birleşik Bedesten API'ye entegre edilmiştir* - -### **Birleşik Bedesten API Araçları (5 Mahkeme) - 🚀 TOKEN OPTİMİZE** -1. `search_bedesten_unified(phrase, court_types, birimAdi, kararTarihiStart, kararTarihiEnd, ...)`: **5 mahkeme türünü** birleşik arama (Yargıtay, Danıştay, Yerel Hukuk, İstinaf Hukuk, KYB) + **79 daire filtreleme** + **Tarih & Kesin Cümle Arama** -2. `get_bedesten_document_markdown(documentId: str)`: Bedesten API'den herhangi bir belgeyi Markdown formatında getirir (HTML/PDF → Markdown) - -### **Emsal Karar Araçları (UYAP)** -3. `search_emsal_detailed_decisions(keyword, ...)`: Emsal (UYAP) kararlarını detaylı kriterlerle arar. -4. `get_emsal_document_markdown(id: str)`: Belirli bir Emsal kararının metnini Markdown formatında getirir. - -### **Uyuşmazlık Mahkemesi Araçları** -5. `search_uyusmazlik_decisions(icerik, ...)`: Uyuşmazlık Mahkemesi kararlarını çeşitli form kriterleriyle arar. -6. `get_uyusmazlik_document_markdown_from_url(document_url)`: Bir Uyuşmazlık kararını tam URL'sinden alıp Markdown formatında getirir. - -### **Anayasa Mahkemesi Araçları (Birleşik API) - 🚀 TOKEN OPTİMİZE** -7. `search_anayasa_unified(decision_type, keywords_all, ...)`: AYM kararlarını birleşik arama (Norm Denetimi + Bireysel Başvuru) - **4 araç → 2 araç optimizasyonu** -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. -### **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. - - ---- - -* **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 - -* **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** - * `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ı - * `search_bddk_decisions(keywords, page)`: BDDK (Bankacılık Düzenleme ve Denetleme Kurumu) kararlarını arar. **"Karar Sayısı" targeting** + **Spesifik URL filtreleme** (`bddk.org.tr/Mevzuat/DokumanGetir`) + **Optimized search** - * `get_bddk_document_markdown(document_id: str, page_number: Optional[int] = 1)`: BDDK kararının tam metnini **sayfalanmış Markdown** formatında getirir (5.000 karakterlik sayfa) - -
- ---- - -
-📊 Kapsamlı İstatistikler & Optimizasyon Başarıları - -🚀 **TOKEN OPTİMİZASYON BAŞARISI:** -- **%61.8 Token Azaltma:** 14,061 → 5,369 tokens (8,692 token tasarrufu) -- **Hedef Aşım:** 10,000 token hedefini 4,631 token aştık -- **Daha Hızlı Yanıt:** Claude AI ile optimize edilmiş etkileşim -- **Korunan İşlevsellik:** %100 özellik desteği devam ediyor - -**GENEL İSTATİSTİKLER:** -- **Toplam Mahkeme/Kurum:** 13 farklı hukuki kurum (KVKK dahil) -- **Toplam MCP Tool:** 19 optimize edilmiş arama ve belge getirme 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ı) -- **Birleşik API:** 10 ayrı Bedesten aracı → 2 birleşik araç (search_bedesten_unified + get_bedesten_document_markdown) -- **API Kaynağı:** Dual/Triple API desteği ile maksimum kapsama -- **Tam Türk Adalet Sistemi:** Yerel mahkemelerden en yüksek mahkemelere kadar - -**🏛️ Desteklenen Mahkeme Hiyerarşisi:** -``` -Yerel Mahkemeler → İstinaf → Yargıtay/Danıştay → Anayasa Mahkemesi - ↓ ↓ ↓ ↓ -Bedesten API Bedesten API Dual/Triple API Norm+Bireysel API -+ Tarih + Kesin + Tarih + Kesin + Daire + Tarih + Gelişmiş - Cümle Arama Cümle Arama + Kesin Cümle Arama -``` - -**⚖️ Kapsamlı Filtreleme Özellikleri:** -- **Daire Filtreleme:** 79 seçenek (52 Yargıtay + 27 Danıştay) - - **Yargıtay:** 52 seçenek (1-23 Hukuk, 1-23 Ceza, Genel Kurullar, Başkanlar Kurulu) - - **Danıştay:** 27 seçenek (1-17 Daireler, İdare/Vergi Kurulları, Askeri Mahkemeler) -- **Tarih Filtreleme:** 5 Bedesten API aracında ISO 8601 formatı (YYYY-MM-DDTHH:MM:SS.000Z) - - Tek tarih, tarih aralığı, tek taraflı filtreleme desteği - - Yargıtay, Danıştay, Yerel Hukuk, İstinaf Hukuk, KYB kararları -- **Kesin Cümle Arama:** 5 Bedesten API aracında çift tırnak formatı - - Normal arama: `"mülkiyet kararı"` (kelimeler ayrı ayrı) - - Kesin arama: `"\"mülkiyet kararı\""` (tam cümle olarak) - - Daha kesin sonuçlar için hukuki terimler ve kavramlar - -**🔧 OPTİMİZASYON DETAYLARI:** -- **Anayasa Mahkemesi:** 4 araç → 2 birleşik araç (search_anayasa_unified + get_anayasa_document_unified) -- **Yargıtay & Danıştay:** Ana API araçları birleşik Bedesten API'ye entegre edildi -- **Sayıştay:** 6 araç → 2 birleşik araç (search_sayistay_unified + get_sayistay_document_unified) -- **Parameter Optimizasyonu:** pageSize parametreleri optimize edildi -- **Açıklama Optimizasyonu:** Uzun açıklamalar kısaltıldı (örn: KIK karar_metni) - -
- ---- - -
-🌐 Web Service / ASGI Deployment - -Yargı MCP artık web servisi olarak da çalıştırılabilir! ASGI desteği sayesinde: - -- **Web API olarak erişim**: HTTP endpoint'leri üzerinden MCP araçlarına erişim -- **Cloud deployment**: Heroku, Railway, Google Cloud Run, AWS Lambda desteği -- **Docker desteği**: Production-ready Docker container -- **FastAPI entegrasyonu**: REST API ve interaktif dokümantasyon - -**Hızlı başlangıç:** -```bash -# ASGI dependencies yükle -pip install yargi-mcp[asgi] - -# Web servisi olarak başlat -python run_asgi.py -# veya -uvicorn asgi_app:app --host 0.0.0.0 --port 8000 -``` - -Detaylı deployment rehberi için: [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) - -
- ---- - -📜 **Lisans** - -Bu proje MIT Lisansı altında lisanslanmıştır. Detaylar için `LICENSE` dosyasına bakınız. diff --git a/saidsurucu-yargi-mcp-f5fa007/__main__.py b/saidsurucu-yargi-mcp-f5fa007/__main__.py deleted file mode 100644 index 12ab0a0..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/__main__.py +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python3 -"""Entry point for yargi-mcp package.""" - -from mcp_server_main import main - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/bireysel_client.py b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/bireysel_client.py deleted file mode 100644 index 3d01787..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/bireysel_client.py +++ /dev/null @@ -1,355 +0,0 @@ -# anayasa_mcp_module/bireysel_client.py -# This client is for Bireysel Başvuru: https://kararlarbilgibankasi.anayasa.gov.tr - -import httpx -from bs4 import BeautifulSoup, Tag -from typing import Dict, Any, List, Optional, Tuple -import logging -import html -import re -import io -from urllib.parse import urlencode, urljoin, quote -from markitdown import MarkItDown -import math # For math.ceil for pagination - -from .models import ( - AnayasaBireyselReportSearchRequest, - AnayasaBireyselReportDecisionDetail, - AnayasaBireyselReportDecisionSummary, - AnayasaBireyselReportSearchResult, - AnayasaBireyselBasvuruDocumentMarkdown, # Model for Bireysel Başvuru document -) - -logger = logging.getLogger(__name__) -if not logger.hasHandlers(): - logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - - -class AnayasaBireyselBasvuruApiClient: - BASE_URL = "https://kararlarbilgibankasi.anayasa.gov.tr" - SEARCH_PATH = "/Ara" - DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000 # Character limit per page - - def __init__(self, request_timeout: float = 60.0): - self.http_client = httpx.AsyncClient( - base_url=self.BASE_URL, - headers={ - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - }, - timeout=request_timeout, - verify=True, - follow_redirects=True - ) - - def _build_query_params_for_bireysel_report(self, params: AnayasaBireyselReportSearchRequest) -> List[Tuple[str, str]]: - query_params: List[Tuple[str, str]] = [] - query_params.append(("KararBulteni", "1")) # Specific to this report type - - if params.keywords: - for kw in params.keywords: - query_params.append(("KelimeAra[]", kw)) - - if params.page_to_fetch and params.page_to_fetch > 1: - query_params.append(("page", str(params.page_to_fetch))) - - return query_params - - async def search_bireysel_basvuru_report( - self, - params: AnayasaBireyselReportSearchRequest - ) -> AnayasaBireyselReportSearchResult: - final_query_params = self._build_query_params_for_bireysel_report(params) - request_url = self.SEARCH_PATH - - logger.info(f"AnayasaBireyselBasvuruApiClient: Performing Bireysel Başvuru Report search. Path: {request_url}, Params: {final_query_params}") - - try: - response = await self.http_client.get(request_url, params=final_query_params) - response.raise_for_status() - html_content = response.text - except httpx.RequestError as e: - logger.error(f"AnayasaBireyselBasvuruApiClient: HTTP request error during Bireysel Başvuru Report search: {e}") - raise - except Exception as e: - logger.error(f"AnayasaBireyselBasvuruApiClient: Error processing Bireysel Başvuru Report search request: {e}") - raise - - soup = BeautifulSoup(html_content, 'html.parser') - - total_records = None - bulunan_karar_div = soup.find("div", class_="bulunankararsayisi") - if bulunan_karar_div: - match_records = re.search(r'(\d+)\s*Karar Bulundu', bulunan_karar_div.get_text(strip=True)) - if match_records: - total_records = int(match_records.group(1)) - - processed_decisions: List[AnayasaBireyselReportDecisionSummary] = [] - - report_content_area = soup.find("div", class_="HaberBulteni") - if not report_content_area: - logger.warning("HaberBulteni div not found, attempting to parse decision divs from the whole page.") - report_content_area = soup - - decision_divs = report_content_area.find_all("div", class_="KararBulteniBirKarar") - if not decision_divs: - logger.warning("No KararBulteniBirKarar divs found.") - - - for decision_div in decision_divs: - title_tag = decision_div.find("h4") - title_text = title_tag.get_text(strip=True) if title_tag and title_tag.strong else (title_tag.get_text(strip=True) if title_tag else "") - - - alti_cizili_div = decision_div.find("div", class_="AltiCizili") - ref_no, dec_type, body, app_date, dec_date, url_path = "", "", "", "", "", "" - if alti_cizili_div: - link_tag = alti_cizili_div.find("a", href=True) - if link_tag: - ref_no = link_tag.get_text(strip=True) - url_path = link_tag['href'] - - parts_text = alti_cizili_div.get_text(separator="|", strip=True) - parts = [part.strip() for part in parts_text.split("|")] - - # Clean ref_no from the first part if it was extracted from link - if ref_no and parts and parts[0].strip().startswith(ref_no): - parts[0] = parts[0].replace(ref_no, "").strip() - if not parts[0]: parts.pop(0) # Remove empty string if ref_no was the only content - - # Assign parts based on typical order, adjusting for missing ref_no at start - current_idx = 0 - if not ref_no and len(parts) > current_idx and re.match(r"\d+/\d+", parts[current_idx]): # Check if first part is ref_no - ref_no = parts[current_idx] - current_idx += 1 - - dec_type = parts[current_idx] if len(parts) > current_idx else "" - current_idx += 1 - body = parts[current_idx] if len(parts) > current_idx else "" - current_idx += 1 - - app_date_raw = parts[current_idx] if len(parts) > current_idx else "" - current_idx += 1 - dec_date_raw = parts[current_idx] if len(parts) > current_idx else "" - - if app_date_raw and "Başvuru Tarihi :" in app_date_raw: - app_date = app_date_raw.replace("Başvuru Tarihi :", "").strip() - elif app_date_raw: # If label is missing but format matches - app_date_match = re.search(r'(\d{1,2}/\d{1,2}/\d{4})', app_date_raw) - if app_date_match: app_date = app_date_match.group(1) - - - if dec_date_raw and "Karar Tarihi :" in dec_date_raw: - dec_date = dec_date_raw.replace("Karar Tarihi :", "").strip() - elif dec_date_raw: # If label is missing but format matches - dec_date_match = re.search(r'(\d{1,2}/\d{1,2}/\d{4})', dec_date_raw) - if dec_date_match: dec_date = dec_date_match.group(1) - - - subject_div = decision_div.find(lambda tag: tag.name == 'div' and not tag.has_attr('class') and tag.get_text(strip=True).startswith("BAŞVURU KONUSU :")) - subject_text = subject_div.get_text(strip=True).replace("BAŞVURU KONUSU :", "").strip() if subject_div else "" - - details_list: List[AnayasaBireyselReportDecisionDetail] = [] - karar_detaylari_div = decision_div.find_next_sibling("div", id="KararDetaylari") # Corrected: was KararDetaylari - if karar_detaylari_div: - table = karar_detaylari_div.find("table", class_="table") - if table and table.find("tbody"): - for row in table.find("tbody").find_all("tr"): - cells = row.find_all("td") - if len(cells) == 4: # Hak, Müdahale İddiası, Sonuç, Giderim - details_list.append(AnayasaBireyselReportDecisionDetail( - hak=cells[0].get_text(strip=True) or "", - mudahale_iddiasi=cells[1].get_text(strip=True) or "", - sonuc=cells[2].get_text(strip=True) or "", - giderim=cells[3].get_text(strip=True) or "", - )) - - full_decision_page_url = urljoin(self.BASE_URL, url_path) if url_path else "" - - processed_decisions.append(AnayasaBireyselReportDecisionSummary( - title=title_text, - decision_reference_no=ref_no, - decision_page_url=full_decision_page_url, - decision_type_summary=dec_type, - decision_making_body=body, - application_date_summary=app_date, - decision_date_summary=dec_date, - application_subject_summary=subject_text, - details=details_list - )) - - return AnayasaBireyselReportSearchResult( - decisions=processed_decisions, - total_records_found=total_records, - retrieved_page_number=params.page_to_fetch - ) - - def _convert_html_to_markdown_bireysel(self, full_decision_html_content: str) -> Optional[str]: - if not full_decision_html_content: - return None - - processed_html = html.unescape(full_decision_html_content) - soup = BeautifulSoup(processed_html, "html.parser") - html_input_for_markdown = "" - - karar_tab_content = soup.find("div", id="Karar") - if karar_tab_content: - karar_html_span = karar_tab_content.find("span", class_="kararHtml") - if karar_html_span: - word_section = karar_html_span.find("div", class_="WordSection1") - if word_section: - for s in word_section.select('script, style, .item.col-xs-12.col-sm-12, center:has(b)'): - s.decompose() - html_input_for_markdown = str(word_section) - else: - logger.warning("AnayasaBireyselBasvuruApiClient: WordSection1 not found in span.kararHtml. Using span.kararHtml content.") - for s in karar_html_span.select('script, style, .item.col-xs-12.col-sm-12, center:has(b)'): - s.decompose() - html_input_for_markdown = str(karar_html_span) - else: - logger.warning("AnayasaBireyselBasvuruApiClient: span.kararHtml not found in div#Karar. Using div#Karar content.") - for s in karar_tab_content.select('script, style, .item.col-xs-12.col-sm-12, center:has(b)'): - s.decompose() - html_input_for_markdown = str(karar_tab_content) - else: - logger.warning("AnayasaBireyselBasvuruApiClient: div#Karar (KARAR tab) not found. Trying WordSection1 fallback.") - word_section_fallback = soup.find("div", class_="WordSection1") - if word_section_fallback: - for s in word_section_fallback.select('script, style, .item.col-xs-12.col-sm-12, center:has(b)'): - s.decompose() - html_input_for_markdown = str(word_section_fallback) - else: - body_tag = soup.find("body") - if body_tag: - for s in body_tag.select('script, style, .item.col-xs-12.col-sm-12, center:has(b), .banner, .footer, .yazdirmaalani, .filtreler, .menu, .altmenu, .geri, .arabuton, .temizlebutonu, form#KararGetir, .TabBaslik, #KararDetaylari, .share-button-container'): - s.decompose() - html_input_for_markdown = str(body_tag) - else: - html_input_for_markdown = processed_html - - markdown_text = None - try: - # Ensure the content is wrapped in basic HTML structure if it's not already - if not html_input_for_markdown.strip().lower().startswith(("{html_input_for_markdown}" - else: - html_content = html_input_for_markdown - - # Convert HTML string to bytes and create BytesIO stream - html_bytes = html_content.encode('utf-8') - html_stream = io.BytesIO(html_bytes) - - # Pass BytesIO stream to MarkItDown to avoid temp file creation - md_converter = MarkItDown() - conversion_result = md_converter.convert(html_stream) - markdown_text = conversion_result.text_content - except Exception as e: - logger.error(f"AnayasaBireyselBasvuruApiClient: MarkItDown conversion error: {e}") - return markdown_text - - async def get_decision_document_as_markdown( - self, - document_url_path: str, # e.g. /BB/2021/20295 - page_number: int = 1 - ) -> AnayasaBireyselBasvuruDocumentMarkdown: - full_url = urljoin(self.BASE_URL, document_url_path) - logger.info(f"AnayasaBireyselBasvuruApiClient: Fetching Bireysel Başvuru document for Markdown (page {page_number}) from URL: {full_url}") - - basvuru_no_from_page = None - karar_tarihi_from_page = None - basvuru_tarihi_from_page = None - karari_veren_birim_from_page = None - karar_turu_from_page = None - resmi_gazete_info_from_page = None - - try: - response = await self.http_client.get(full_url) - response.raise_for_status() - html_content_from_api = response.text - - if not isinstance(html_content_from_api, str) or not html_content_from_api.strip(): - logger.warning(f"AnayasaBireyselBasvuruApiClient: Received empty HTML from {full_url}.") - return AnayasaBireyselBasvuruDocumentMarkdown( - source_url=full_url, markdown_chunk=None, current_page=page_number, total_pages=0, is_paginated=False - ) - - soup = BeautifulSoup(html_content_from_api, 'html.parser') - - meta_desc_tag = soup.find("meta", attrs={"name": "description"}) - if meta_desc_tag and meta_desc_tag.get("content"): - content = meta_desc_tag["content"] - bn_match = re.search(r"B\.\s*No:\s*([\d\/]+)", content) - if bn_match: basvuru_no_from_page = bn_match.group(1).strip() - - date_match = re.search(r"(\d{1,2}\/\d{1,2}\/\d{4}),\s*§", content) - if date_match: karar_tarihi_from_page = date_match.group(1).strip() - - karar_detaylari_tab = soup.find("div", id="KararDetaylari") - if karar_detaylari_tab: - table = karar_detaylari_tab.find("table", class_="table") - if table: - rows = table.find_all("tr") - for row in rows: - cells = row.find_all("td") - if len(cells) == 2: - key = cells[0].get_text(strip=True) - value = cells[1].get_text(strip=True) - if "Kararı Veren Birim" in key: karari_veren_birim_from_page = value - elif "Karar Türü (Başvuru Sonucu)" in key: karar_turu_from_page = value - elif "Başvuru No" in key and not basvuru_no_from_page: basvuru_no_from_page = value - elif "Başvuru Tarihi" in key: basvuru_tarihi_from_page = value - elif "Karar Tarihi" in key and not karar_tarihi_from_page: karar_tarihi_from_page = value - elif "Resmi Gazete Tarih / Sayı" in key: resmi_gazete_info_from_page = value - - full_markdown_content = self._convert_html_to_markdown_bireysel(html_content_from_api) - - if not full_markdown_content: - return AnayasaBireyselBasvuruDocumentMarkdown( - source_url=full_url, - basvuru_no_from_page=basvuru_no_from_page, - karar_tarihi_from_page=karar_tarihi_from_page, - basvuru_tarihi_from_page=basvuru_tarihi_from_page, - karari_veren_birim_from_page=karari_veren_birim_from_page, - karar_turu_from_page=karar_turu_from_page, - resmi_gazete_info_from_page=resmi_gazete_info_from_page, - markdown_chunk=None, - current_page=page_number, - total_pages=0, - is_paginated=False - ) - - content_length = len(full_markdown_content) - total_pages = math.ceil(content_length / self.DOCUMENT_MARKDOWN_CHUNK_SIZE) - if total_pages == 0: total_pages = 1 - - current_page_clamped = max(1, min(page_number, total_pages)) - start_index = (current_page_clamped - 1) * self.DOCUMENT_MARKDOWN_CHUNK_SIZE - end_index = start_index + self.DOCUMENT_MARKDOWN_CHUNK_SIZE - markdown_chunk = full_markdown_content[start_index:end_index] - - return AnayasaBireyselBasvuruDocumentMarkdown( - source_url=full_url, - basvuru_no_from_page=basvuru_no_from_page, - karar_tarihi_from_page=karar_tarihi_from_page, - basvuru_tarihi_from_page=basvuru_tarihi_from_page, - karari_veren_birim_from_page=karari_veren_birim_from_page, - karar_turu_from_page=karar_turu_from_page, - resmi_gazete_info_from_page=resmi_gazete_info_from_page, - markdown_chunk=markdown_chunk, - current_page=current_page_clamped, - total_pages=total_pages, - is_paginated=(total_pages > 1) - ) - - except httpx.RequestError as e: - logger.error(f"AnayasaBireyselBasvuruApiClient: HTTP error fetching Bireysel Başvuru document from {full_url}: {e}") - raise - except Exception as e: - logger.error(f"AnayasaBireyselBasvuruApiClient: General error processing Bireysel Başvuru document from {full_url}: {e}") - raise - - async def close_client_session(self): - if hasattr(self, 'http_client') and self.http_client and not self.http_client.is_closed: - await self.http_client.aclose() - logger.info("AnayasaBireyselBasvuruApiClient: HTTP client session closed.") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/client.py deleted file mode 100644 index 9e15a48..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/client.py +++ /dev/null @@ -1,356 +0,0 @@ -# anayasa_mcp_module/client.py -# This client is for Norm Denetimi: https://normkararlarbilgibankasi.anayasa.gov.tr - -import httpx -from bs4 import BeautifulSoup -from typing import Dict, Any, List, Optional, Tuple -import logging -import html -import re -import io -from urllib.parse import urlencode, urljoin, quote -from markitdown import MarkItDown -import math # For math.ceil for pagination - -from .models import ( - AnayasaNormDenetimiSearchRequest, - AnayasaDecisionSummary, - AnayasaReviewedNormInfo, - AnayasaSearchResult, - AnayasaDocumentMarkdown, # Model for Norm Denetimi document -) - -logger = logging.getLogger(__name__) -if not logger.hasHandlers(): - logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - -class AnayasaMahkemesiApiClient: - BASE_URL = "https://normkararlarbilgibankasi.anayasa.gov.tr" - SEARCH_PATH_SEGMENT = "Ara" - DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000 # Character limit per page - - def __init__(self, request_timeout: float = 60.0): - self.http_client = httpx.AsyncClient( - base_url=self.BASE_URL, - headers={ - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - }, - timeout=request_timeout, - verify=True, - follow_redirects=True - ) - - def _build_search_query_params_for_aym(self, params: AnayasaNormDenetimiSearchRequest) -> List[Tuple[str, str]]: - query_params: List[Tuple[str, str]] = [] - if params.keywords_all: - for kw in params.keywords_all: query_params.append(("KelimeAra[]", kw)) - if params.keywords_any: - for kw in params.keywords_any: query_params.append(("HerhangiBirKelimeAra[]", kw)) - if params.keywords_exclude: - for kw in params.keywords_exclude: query_params.append(("BulunmayanKelimeAra[]", kw)) - if params.period and params.period and params.period != "ALL": query_params.append(("Donemler_id", params.period)) - if params.case_number_esas: query_params.append(("EsasNo", params.case_number_esas)) - if params.decision_number_karar: query_params.append(("KararNo", params.decision_number_karar)) - if params.first_review_date_start: query_params.append(("IlkIncelemeTarihiIlk", params.first_review_date_start)) - if params.first_review_date_end: query_params.append(("IlkIncelemeTarihiSon", params.first_review_date_end)) - if params.decision_date_start: query_params.append(("KararTarihiIlk", params.decision_date_start)) - if params.decision_date_end: query_params.append(("KararTarihiSon", params.decision_date_end)) - if params.application_type and params.application_type and params.application_type != "ALL": query_params.append(("BasvuruTurler_id", params.application_type)) - if params.applicant_general_name: query_params.append(("BasvuranGeneller_id", params.applicant_general_name)) - if params.applicant_specific_name: query_params.append(("BasvuranOzeller_id", params.applicant_specific_name)) - if params.attending_members_names: - for name in params.attending_members_names: query_params.append(("Uyeler_id[]", name)) - if params.rapporteur_name: query_params.append(("Raportorler_id", params.rapporteur_name)) - if params.norm_type and params.norm_type and params.norm_type != "ALL": query_params.append(("NormunTurler_id", params.norm_type)) - if params.norm_id_or_name: query_params.append(("NormunNumarasiAdlar_id", params.norm_id_or_name)) - if params.norm_article: query_params.append(("NormunMaddeNumarasi", params.norm_article)) - if params.review_outcomes: - for outcome_val in params.review_outcomes: - if outcome_val and outcome_val != "ALL": query_params.append(("IncelemeTuruKararSonuclar_id[]", outcome_val)) - if params.reason_for_final_outcome and params.reason_for_final_outcome and params.reason_for_final_outcome != "ALL": - query_params.append(("KararSonucununGerekcesi", params.reason_for_final_outcome)) - if params.basis_constitution_article_numbers: - for article_no in params.basis_constitution_article_numbers: query_params.append(("DayanakHukmu[]", article_no)) - if params.official_gazette_date_start: query_params.append(("ResmiGazeteTarihiIlk", params.official_gazette_date_start)) - if params.official_gazette_date_end: query_params.append(("ResmiGazeteTarihiSon", params.official_gazette_date_end)) - if params.official_gazette_number_start: query_params.append(("ResmiGazeteSayisiIlk", params.official_gazette_number_start)) - if params.official_gazette_number_end: query_params.append(("ResmiGazeteSayisiSon", params.official_gazette_number_end)) - if params.has_press_release and params.has_press_release and params.has_press_release != "ALL": query_params.append(("BasinDuyurusu", params.has_press_release)) - if params.has_dissenting_opinion and params.has_dissenting_opinion and params.has_dissenting_opinion != "ALL": query_params.append(("KarsiOy", params.has_dissenting_opinion)) - if params.has_different_reasoning and params.has_different_reasoning and params.has_different_reasoning != "ALL": query_params.append(("FarkliGerekce", params.has_different_reasoning)) - - # Add pagination and sorting parameters as query params instead of URL path - if params.results_per_page and params.results_per_page != 10: - query_params.append(("SatirSayisi", str(params.results_per_page))) - - if params.sort_by_criteria and params.sort_by_criteria != "KararTarihi": - query_params.append(("Siralama", params.sort_by_criteria)) - - if params.page_to_fetch and params.page_to_fetch > 1: - query_params.append(("page", str(params.page_to_fetch))) - return query_params - - async def search_norm_denetimi_decisions( - self, - params: AnayasaNormDenetimiSearchRequest - ) -> AnayasaSearchResult: - # Use simple /Ara endpoint - the complex path structure seems to cause 404s - request_path = f"/{self.SEARCH_PATH_SEGMENT}" - - final_query_params = self._build_search_query_params_for_aym(params) - logger.info(f"AnayasaMahkemesiApiClient: Performing Norm Denetimi search. Path: {request_path}, Params: {final_query_params}") - - try: - response = await self.http_client.get(request_path, params=final_query_params) - response.raise_for_status() - html_content = response.text - except httpx.RequestError as e: - logger.error(f"AnayasaMahkemesiApiClient: HTTP request error during Norm Denetimi search: {e}") - raise - except Exception as e: - logger.error(f"AnayasaMahkemesiApiClient: Error processing Norm Denetimi search request: {e}") - raise - - soup = BeautifulSoup(html_content, 'html.parser') - - total_records = None - bulunan_karar_div = soup.find("div", class_="bulunankararsayisi") - if not bulunan_karar_div: # Fallback for mobile view - bulunan_karar_div = soup.find("div", class_="bulunankararsayisiMobil") - - if bulunan_karar_div: - match_records = re.search(r'(\d+)\s*Karar Bulundu', bulunan_karar_div.get_text(strip=True)) - if match_records: - total_records = int(match_records.group(1)) - - processed_decisions: List[AnayasaDecisionSummary] = [] - decision_divs = soup.find_all("div", class_="birkarar") - - for decision_div in decision_divs: - link_tag = decision_div.find("a", href=True) - doc_url_path = link_tag['href'] if link_tag else None - decision_page_url_str = urljoin(self.BASE_URL, doc_url_path) if doc_url_path else None - - title_div = decision_div.find("div", class_="bkararbaslik") - ek_no_text_raw = title_div.get_text(strip=True, separator=" ").replace('\xa0', ' ') if title_div else "" - ek_no_match = re.search(r"(E\.\s*\d+/\d+\s*,\s*K\.\s*\d+/\d+)", ek_no_text_raw) - ek_no_text = ek_no_match.group(1) if ek_no_match else ek_no_text_raw.split("Sayılı Karar")[0].strip() - - keyword_count_div = title_div.find("div", class_="BulunanKelimeSayisi") if title_div else None - keyword_count_text = keyword_count_div.get_text(strip=True).replace("Bulunan Kelime Sayısı", "").strip() if keyword_count_div else None - keyword_count = int(keyword_count_text) if keyword_count_text and keyword_count_text.isdigit() else None - - info_div = decision_div.find("div", class_="kararbilgileri") - info_parts = [part.strip() for part in info_div.get_text(separator="|").split("|")] if info_div else [] - - app_type_summary = info_parts[0] if len(info_parts) > 0 else None - applicant_summary = info_parts[1] if len(info_parts) > 1 else None - outcome_summary = info_parts[2] if len(info_parts) > 2 else None - dec_date_raw = info_parts[3] if len(info_parts) > 3 else None - decision_date_summary = dec_date_raw.replace("Karar Tarihi:", "").strip() if dec_date_raw else None - - reviewed_norms_list: List[AnayasaReviewedNormInfo] = [] - details_table_container = decision_div.find_next_sibling("div", class_=re.compile(r"col-sm-12")) # The details table is in a sibling div - if details_table_container: - details_table = details_table_container.find("table", class_="table") - if details_table and details_table.find("tbody"): - for row in details_table.find("tbody").find_all("tr"): - cells = row.find_all("td") - if len(cells) == 6: - reviewed_norms_list.append(AnayasaReviewedNormInfo( - norm_name_or_number=cells[0].get_text(strip=True) or None, - article_number=cells[1].get_text(strip=True) or None, - review_type_and_outcome=cells[2].get_text(strip=True) or None, - outcome_reason=cells[3].get_text(strip=True) or None, - basis_constitution_articles_cited=[a.strip() for a in cells[4].get_text(strip=True).split(',') if a.strip()] if cells[4].get_text(strip=True) else [], - postponement_period=cells[5].get_text(strip=True) or None - )) - - processed_decisions.append(AnayasaDecisionSummary( - decision_reference_no=ek_no_text, - decision_page_url=decision_page_url_str, - keywords_found_count=keyword_count, - application_type_summary=app_type_summary, - applicant_summary=applicant_summary, - decision_outcome_summary=outcome_summary, - decision_date_summary=decision_date_summary, - reviewed_norms=reviewed_norms_list - )) - - return AnayasaSearchResult( - decisions=processed_decisions, - total_records_found=total_records, - retrieved_page_number=params.page_to_fetch - ) - - def _convert_html_to_markdown_norm_denetimi(self, full_decision_html_content: str) -> Optional[str]: - """Converts direct HTML content from an Anayasa Mahkemesi Norm Denetimi decision page to Markdown.""" - if not full_decision_html_content: - return None - - processed_html = html.unescape(full_decision_html_content) - soup = BeautifulSoup(processed_html, "html.parser") - html_input_for_markdown = "" - - karar_tab_content = soup.find("div", id="Karar") # "KARAR" tab content - if karar_tab_content: - karar_metni_div = karar_tab_content.find("div", class_="KararMetni") - if karar_metni_div: - # Remove scripts and styles - for script_tag in karar_metni_div.find_all("script"): script_tag.decompose() - for style_tag in karar_metni_div.find_all("style"): style_tag.decompose() - # Remove "Künye Kopyala" button and other non-content divs - for item_div in karar_metni_div.find_all("div", class_="item col-sm-12"): item_div.decompose() - for modal_div in karar_metni_div.find_all("div", class_="modal fade"): modal_div.decompose() # If any modals - - word_section = karar_metni_div.find("div", class_="WordSection1") - html_input_for_markdown = str(word_section) if word_section else str(karar_metni_div) - else: - html_input_for_markdown = str(karar_tab_content) - else: - # Fallback if specific structure is not found - word_section_fallback = soup.find("div", class_="WordSection1") - if word_section_fallback: - html_input_for_markdown = str(word_section_fallback) - else: - # Last resort: use the whole body or the raw HTML - body_tag = soup.find("body") - html_input_for_markdown = str(body_tag) if body_tag else processed_html - - markdown_text = None - try: - # Ensure the content is wrapped in basic HTML structure if it's not already - if not html_input_for_markdown.strip().lower().startswith(("{html_input_for_markdown}" - else: - html_content = html_input_for_markdown - - # Convert HTML string to bytes and create BytesIO stream - html_bytes = html_content.encode('utf-8') - html_stream = io.BytesIO(html_bytes) - - # Pass BytesIO stream to MarkItDown to avoid temp file creation - md_converter = MarkItDown() - conversion_result = md_converter.convert(html_stream) - markdown_text = conversion_result.text_content - except Exception as e: - logger.error(f"AnayasaMahkemesiApiClient: MarkItDown conversion error: {e}") - return markdown_text - - async def get_decision_document_as_markdown( - self, - document_url: str, - page_number: int = 1 - ) -> AnayasaDocumentMarkdown: - """ - Retrieves a specific Anayasa Mahkemesi (Norm Denetimi) decision, - converts its content to Markdown, and returns the requested page/chunk. - """ - full_url = urljoin(self.BASE_URL, document_url) if not document_url.startswith("http") else document_url - logger.info(f"AnayasaMahkemesiApiClient: Fetching Norm Denetimi document for Markdown (page {page_number}) from URL: {full_url}") - - decision_ek_no_from_page = None - decision_date_from_page = None - official_gazette_from_page = None - - try: - # Use a new client instance for document fetching if headers/timeout needs to be different, - # or reuse self.http_client if settings are compatible. For now, self.http_client. - get_response = await self.http_client.get(full_url, headers={"Accept": "text/html"}) - get_response.raise_for_status() - html_content_from_api = get_response.text - - if not isinstance(html_content_from_api, str) or not html_content_from_api.strip(): - logger.warning(f"AnayasaMahkemesiApiClient: Received empty or non-string HTML from URL {full_url}.") - return AnayasaDocumentMarkdown( - source_url=full_url, markdown_chunk=None, current_page=page_number, total_pages=0, is_paginated=False - ) - - # Extract metadata from the page content (E.K. No, Date, RG) - soup = BeautifulSoup(html_content_from_api, "html.parser") - karar_metni_div = soup.find("div", class_="KararMetni") # Usually within div#Karar - if not karar_metni_div: # Fallback if not in KararMetni - karar_metni_div = soup.find("div", class_="WordSection1") - - # Initialize with empty string defaults - decision_ek_no_from_page = "" - decision_date_from_page = "" - official_gazette_from_page = "" - - if karar_metni_div: - # Attempt to find E.K. No (Esas No, Karar No) - # Norm Denetimi pages often have this in bold

tags directly or in the WordSection1 - # Look for patterns like "Esas No.: YYYY/NN" and "Karar No.: YYYY/NN" - - esas_no_tag = karar_metni_div.find(lambda tag: tag.name == "p" and tag.find("b") and "Esas No.:" in tag.find("b").get_text()) - karar_no_tag = karar_metni_div.find(lambda tag: tag.name == "p" and tag.find("b") and "Karar No.:" in tag.find("b").get_text()) - karar_tarihi_tag = karar_metni_div.find(lambda tag: tag.name == "p" and tag.find("b") and "Karar tarihi:" in tag.find("b").get_text()) # Less common on Norm pages - resmi_gazete_tag = karar_metni_div.find(lambda tag: tag.name == "p" and ("Resmî Gazete tarih ve sayısı:" in tag.get_text() or "Resmi Gazete tarih/sayı:" in tag.get_text())) - - - if esas_no_tag and esas_no_tag.find("b") and karar_no_tag and karar_no_tag.find("b"): - esas_str = esas_no_tag.find("b").get_text(strip=True).replace('Esas No.:', '').strip() - karar_str = karar_no_tag.find("b").get_text(strip=True).replace('Karar No.:', '').strip() - decision_ek_no_from_page = f"E.{esas_str}, K.{karar_str}" - - if karar_tarihi_tag and karar_tarihi_tag.find("b"): - decision_date_from_page = karar_tarihi_tag.find("b").get_text(strip=True).replace("Karar tarihi:", "").strip() - elif karar_metni_div: # Fallback for Karar Tarihi if not in specific tag - date_match = re.search(r"Karar Tarihi\s*:\s*([\d\.]+)", karar_metni_div.get_text()) # Norm pages often use DD.MM.YYYY - if date_match: decision_date_from_page = date_match.group(1).strip() - - - if resmi_gazete_tag: - # Try to get the bold part first if it exists - bold_rg_tag = resmi_gazete_tag.find("b") - rg_text_content = bold_rg_tag.get_text(strip=True) if bold_rg_tag else resmi_gazete_tag.get_text(strip=True) - official_gazette_from_page = rg_text_content.replace("Resmî Gazete tarih ve sayısı:", "").replace("Resmi Gazete tarih/sayı:", "").strip() - - - full_markdown_content = self._convert_html_to_markdown_norm_denetimi(html_content_from_api) - - if not full_markdown_content: - return AnayasaDocumentMarkdown( - source_url=full_url, - decision_reference_no_from_page=decision_ek_no_from_page, - decision_date_from_page=decision_date_from_page, - official_gazette_info_from_page=official_gazette_from_page, - markdown_chunk=None, - current_page=page_number, - total_pages=0, - is_paginated=False - ) - - content_length = len(full_markdown_content) - total_pages = math.ceil(content_length / self.DOCUMENT_MARKDOWN_CHUNK_SIZE) - if total_pages == 0: total_pages = 1 - - current_page_clamped = max(1, min(page_number, total_pages)) - start_index = (current_page_clamped - 1) * self.DOCUMENT_MARKDOWN_CHUNK_SIZE - end_index = start_index + self.DOCUMENT_MARKDOWN_CHUNK_SIZE - markdown_chunk = full_markdown_content[start_index:end_index] - - return AnayasaDocumentMarkdown( - source_url=full_url, - decision_reference_no_from_page=decision_ek_no_from_page, - decision_date_from_page=decision_date_from_page, - official_gazette_info_from_page=official_gazette_from_page, - markdown_chunk=markdown_chunk, - current_page=current_page_clamped, - total_pages=total_pages, - is_paginated=(total_pages > 1) - ) - - except httpx.RequestError as e: - logger.error(f"AnayasaMahkemesiApiClient: HTTP error fetching Norm Denetimi document from {full_url}: {e}") - raise - except Exception as e: - logger.error(f"AnayasaMahkemesiApiClient: General error processing Norm Denetimi document from {full_url}: {e}") - raise - - async def close_client_session(self): - if hasattr(self, 'http_client') and self.http_client and not self.http_client.is_closed: - await self.http_client.aclose() - logger.info("AnayasaMahkemesiApiClient (Norm Denetimi): HTTP client session closed.") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/models.py deleted file mode 100644 index f5e74e1..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/models.py +++ /dev/null @@ -1,230 +0,0 @@ -# anayasa_mcp_module/models.py - -from pydantic import BaseModel, Field, HttpUrl -from typing import List, Optional, Dict, Any, Literal -from enum import Enum - -# --- Enums (AnayasaDonemEnum, etc. - same as before) --- -class AnayasaDonemEnum(str, Enum): - TUMU = "ALL" - DONEM_1961 = "1" - DONEM_1982 = "2" - - -class AnayasaVarYokEnum(str, Enum): - TUMU = "ALL" - YOK = "0" - VAR = "1" - - -class AnayasaIncelemeSonucuEnum(str, Enum): - TUMU = "ALL" - ESAS_ACILMAMIS_SAYILMA = "1" - ESAS_IPTAL = "2" - ESAS_KARAR_YER_OLMADIGI = "3" - ESAS_RET = "4" - ILK_ACILMAMIS_SAYILMA = "5" - ILK_ISIN_GERI_CEVRILMESI = "6" - ILK_KARAR_YER_OLMADIGI = "7" - ILK_RET = "8" - KANUN_6216_M43_4_IPTAL = "12" - -class AnayasaSonucGerekcesiEnum(str, Enum): - TUMU = "ALL" - ANAYASAYA_AYKIRI_DEGIL = "29" - ANAYASAYA_ESAS_YONUNDEN_AYKIRILIK = "1" - ANAYASAYA_ESAS_YONUNDEN_UYGUNLUK = "2" - ANAYASAYA_SEKIL_ESAS_UYGUNLUK = "30" - ANAYASAYA_SEKIL_YONUNDEN_AYKIRILIK = "3" - ANAYASAYA_SEKIL_YONUNDEN_UYGUNLUK = "4" - AYKIRILIK_ANAYASAYA_ESAS_YONUNDEN_DUPLICATE = "27" - BASVURU_KARARI = "5" - DENETIM_DISI = "6" - DIGER_GEREKCE_1 = "7" - DIGER_GEREKCE_2 = "8" - EKSIKLIGIN_GIDERILMEMESI = "9" - GEREKCE = "10" - GOREV = "11" - GOREV_YETKI = "12" - GOREVLI_MAHKEME = "13" - GORULMEKTE_OLAN_DAVA = "14" - MAHKEME = "15" - NORMDA_DEGISIKLIK_YAPILMASI = "16" - NORMUN_YURURLUKTEN_KALDIRILMASI = "17" - ON_YIL_YASAGI = "18" - SURE = "19" - USULE_UYMAMA = "20" - UYGULANACAK_NORM = "21" - UYGULANAMAZ_HALE_GELME = "22" - YETKI = "23" - YETKI_SURE = "24" - YOK_HUKMUNDE_OLMAMA = "25" - YOKLUK = "26" -# --- End Enums --- - -class AnayasaNormDenetimiSearchRequest(BaseModel): - """Model for Anayasa Mahkemesi (Norm Denetimi) search request for the MCP tool.""" - keywords_all: Optional[List[str]] = Field(default_factory=list, description="Keywords for AND logic (KelimeAra[]).") - keywords_any: Optional[List[str]] = Field(default_factory=list, description="Keywords for OR logic (HerhangiBirKelimeAra[]).") - keywords_exclude: Optional[List[str]] = Field(default_factory=list, description="Keywords to exclude (BulunmayanKelimeAra[]).") - period: Optional[Literal["ALL", "1", "2"]] = Field(default="ALL", description="Constitutional period (Donemler_id).") - case_number_esas: str = Field("", description="Case registry number (EsasNo), e.g., '2023/123'.") - decision_number_karar: str = Field("", description="Decision number (KararNo), e.g., '2023/456'.") - first_review_date_start: str = Field("", description="First review start date (IlkIncelemeTarihiIlk), format DD/MM/YYYY.") - first_review_date_end: str = Field("", description="First review end date (IlkIncelemeTarihiSon), format DD/MM/YYYY.") - decision_date_start: str = Field("", description="Decision start date (KararTarihiIlk), format DD/MM/YYYY.") - decision_date_end: str = Field("", description="Decision end date (KararTarihiSon), format DD/MM/YYYY.") - application_type: Optional[Literal["ALL", "1", "2", "3"]] = Field(default="ALL", description="Type of application (BasvuruTurler_id).") - applicant_general_name: str = Field("", description="General applicant name (BasvuranGeneller_id).") - applicant_specific_name: str = Field("", description="Specific applicant name (BasvuranOzeller_id).") - official_gazette_date_start: str = Field("", description="Official Gazette start date (ResmiGazeteTarihiIlk), format DD/MM/YYYY.") - official_gazette_date_end: str = Field("", description="Official Gazette end date (ResmiGazeteTarihiSon), format DD/MM/YYYY.") - official_gazette_number_start: str = Field("", description="Official Gazette starting number (ResmiGazeteSayisiIlk).") - official_gazette_number_end: str = Field("", description="Official Gazette ending number (ResmiGazeteSayisiSon).") - has_press_release: Optional[Literal["ALL", "0", "1"]] = Field(default="ALL", description="Press release available (BasinDuyurusu).") - has_dissenting_opinion: Optional[Literal["ALL", "0", "1"]] = Field(default="ALL", description="Dissenting opinion exists (KarsiOy).") - has_different_reasoning: Optional[Literal["ALL", "0", "1"]] = Field(default="ALL", description="Different reasoning exists (FarkliGerekce).") - attending_members_names: Optional[List[str]] = Field(default_factory=list, description="List of attending members' exact names (Uyeler_id[]).") - rapporteur_name: str = Field("", description="Rapporteur's exact name (Raportorler_id).") - norm_type: Optional[Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "0"]] = Field(default="ALL", description="Type of the reviewed norm (NormunTurler_id).") - norm_id_or_name: str = Field("", description="Number or name of the norm (NormunNumarasiAdlar_id).") - norm_article: str = Field("", description="Article number of the norm (NormunMaddeNumarasi).") - review_outcomes: Optional[List[Literal["1", "2", "3", "4", "5", "6", "7", "8", "12"]]] = Field(default_factory=list, description="List of review types and outcomes (IncelemeTuruKararSonuclar_id[]).") - reason_for_final_outcome: Optional[Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "29", "30"]] = Field(default="ALL", description="Main reason for the decision outcome (KararSonucununGerekcesi).") - basis_constitution_article_numbers: Optional[List[str]] = Field(default_factory=list, description="List of supporting Constitution article numbers (DayanakHukmu[]).") - results_per_page: int = Field(10, ge=1, le=10, description="Results per page.") - page_to_fetch: int = Field(1, ge=1, description="Page number to fetch for results list.") - sort_by_criteria: str = Field("KararTarihi", description="Sort criteria. Options: 'KararTarihi', 'YayinTarihi', 'Toplam' (keyword count).") - -class AnayasaReviewedNormInfo(BaseModel): - """Details of a norm reviewed within an AYM decision summary.""" - norm_name_or_number: str = Field("", description="Norm name or number") - article_number: str = Field("", description="Article number") - review_type_and_outcome: str = Field("", description="Review type and outcome") - outcome_reason: str = Field("", description="Outcome reason") - basis_constitution_articles_cited: List[str] = Field(default_factory=list) - postponement_period: str = Field("", description="Postponement period") - -class AnayasaDecisionSummary(BaseModel): - """Model for a single Anayasa Mahkemesi (Norm Denetimi) decision summary from search results.""" - decision_reference_no: str = Field("", description="Decision reference number") - decision_page_url: str = Field("", description="Decision page URL") - keywords_found_count: Optional[int] = Field(0, description="Keywords found count") - application_type_summary: str = Field("", description="Application type summary") - applicant_summary: str = Field("", description="Applicant summary") - decision_outcome_summary: str = Field("", description="Decision outcome summary") - decision_date_summary: str = Field("", description="Decision date summary") - reviewed_norms: List[AnayasaReviewedNormInfo] = Field(default_factory=list) - -class AnayasaSearchResult(BaseModel): - """Model for the overall search result for Anayasa Mahkemesi Norm Denetimi decisions.""" - decisions: List[AnayasaDecisionSummary] - total_records_found: int = Field(0, description="Total records found") - retrieved_page_number: int = Field(1, description="Retrieved page number") - -class AnayasaDocumentMarkdown(BaseModel): - """ - Model for an Anayasa Mahkemesi (Norm Denetimi) decision document, containing a chunk of Markdown content - and pagination information. - """ - source_url: HttpUrl - decision_reference_no_from_page: str = Field("", description="E.K. No parsed from the document page.") - decision_date_from_page: str = Field("", description="Decision date parsed from the document page.") - official_gazette_info_from_page: str = Field("", description="Official Gazette info parsed from the document page.") - markdown_chunk: str = Field("", description="A 5,000 character chunk of the Markdown content.") # Corrected chunk size - current_page: int = Field(description="The current page number of the markdown chunk (1-indexed).") - total_pages: int = Field(description="Total number of pages for the full markdown content.") - is_paginated: bool = Field(description="True if the full markdown content is split into multiple pages.") - - -# --- Models for Anayasa Mahkemesi - Bireysel Başvuru Karar Raporu --- - -class AnayasaBireyselReportSearchRequest(BaseModel): - """Model for Anayasa Mahkemesi (Bireysel Başvuru) 'Karar Arama Raporu' search request.""" - keywords: Optional[List[str]] = Field(default_factory=list, description="Keywords for AND logic (KelimeAra[]).") - page_to_fetch: int = Field(1, ge=1, description="Page number to fetch for the report (page). Default is 1.") - -class AnayasaBireyselReportDecisionDetail(BaseModel): - """Details of a specific right/claim within a Bireysel Başvuru decision summary in a report.""" - hak: str = Field("", description="İhlal edildiği iddia edilen hak (örneğin, Mülkiyet hakkı).") - mudahale_iddiasi: str = Field("", description="İhlale neden olan müdahale iddiası.") - sonuc: str = Field("", description="İnceleme sonucu (örneğin, İhlal, Düşme).") - giderim: str = Field("", description="Kararlaştırılan giderim (örneğin, Yeniden yargılama).") - -class AnayasaBireyselReportDecisionSummary(BaseModel): - """Model for a single Anayasa Mahkemesi (Bireysel Başvuru) decision summary from a 'Karar Arama Raporu'.""" - title: str = Field("", description="Başvurunun başlığı (e.g., 'HASAN DURMUŞ Başvurusuna İlişkin Karar').") - decision_reference_no: str = Field("", description="Başvuru Numarası (e.g., '2019/19126').") - decision_page_url: str = Field("", description="URL to the full decision page.") - decision_type_summary: str = Field("", description="Karar Türü (Başvuru Sonucu) (e.g., 'Esas (İhlal)').") - decision_making_body: str = Field("", description="Kararı Veren Birim (e.g., 'Genel Kurul', 'Birinci Bölüm').") - application_date_summary: str = Field("", description="Başvuru Tarihi (DD/MM/YYYY).") - decision_date_summary: str = Field("", description="Karar Tarihi (DD/MM/YYYY).") - application_subject_summary: str = Field("", description="Başvuru konusunun özeti.") - details: List[AnayasaBireyselReportDecisionDetail] = Field(default_factory=list, description="İncelenen haklar ve sonuçlarına ilişkin detaylar.") - -class AnayasaBireyselReportSearchResult(BaseModel): - """Model for the overall search result for Anayasa Mahkemesi 'Karar Arama Raporu'.""" - decisions: List[AnayasaBireyselReportDecisionSummary] - total_records_found: int = Field(0, description="Raporda bulunan toplam karar sayısı.") - retrieved_page_number: int = Field(description="Alınan rapor sayfa numarası.") - - -class AnayasaBireyselBasvuruDocumentMarkdown(BaseModel): - """ - Model for an Anayasa Mahkemesi (Bireysel Başvuru) decision document, containing a chunk of Markdown content - and pagination information. Fetched from /BB/YYYY/NNNN paths. - """ - source_url: HttpUrl - basvuru_no_from_page: Optional[str] = Field(None, description="Başvuru Numarası (B.No) parsed from the document page.") - karar_tarihi_from_page: Optional[str] = Field(None, description="Decision date parsed from the document page.") - basvuru_tarihi_from_page: Optional[str] = Field(None, description="Application date parsed from the document page.") - karari_veren_birim_from_page: Optional[str] = Field(None, description="Deciding body (Bölüm/Genel Kurul) parsed from the document page.") - karar_turu_from_page: Optional[str] = Field(None, description="Decision type (Başvuru Sonucu) parsed from the document page.") - resmi_gazete_info_from_page: Optional[str] = Field(None, description="Official Gazette info parsed from the document page, if available.") - markdown_chunk: Optional[str] = Field(None, description="A 5,000 character chunk of the Markdown content.") - current_page: int = Field(description="The current page number of the markdown chunk (1-indexed).") - total_pages: int = Field(description="Total number of pages for the full markdown content.") - is_paginated: bool = Field(description="True if the full markdown content is split into multiple pages.") - -# --- End Models for Bireysel Başvuru --- - -# --- Unified Models --- -class AnayasaUnifiedSearchRequest(BaseModel): - """Unified search request for both Norm Denetimi and Bireysel Başvuru.""" - decision_type: Literal["norm_denetimi", "bireysel_basvuru"] = Field(..., description="Decision type: norm_denetimi or bireysel_basvuru") - - # Common parameters - keywords: List[str] = Field(default_factory=list, description="Keywords to search for") - page_to_fetch: int = Field(1, ge=1, le=100, description="Page number to fetch (1-100)") - results_per_page: int = Field(10, ge=1, le=100, description="Results per page (1-100)") - - # Norm Denetimi specific parameters (ignored for bireysel_basvuru) - keywords_all: List[str] = Field(default_factory=list, description="All keywords must be present (norm_denetimi only)") - keywords_any: List[str] = Field(default_factory=list, description="Any of these keywords (norm_denetimi only)") - decision_type_norm: Literal["ALL", "1", "2", "3"] = Field("ALL", description="Decision type for norm denetimi") - application_date_start: str = Field("", description="Application start date (norm_denetimi only)") - application_date_end: str = Field("", description="Application end date (norm_denetimi only)") - - # Bireysel Başvuru specific parameters (ignored for norm_denetimi) - decision_start_date: str = Field("", description="Decision start date (bireysel_basvuru only)") - decision_end_date: str = Field("", description="Decision end date (bireysel_basvuru only)") - norm_type: Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "0"] = Field("ALL", description="Norm type (bireysel_basvuru only)") - subject_category: str = Field("", description="Subject category (bireysel_basvuru only)") - -class AnayasaUnifiedSearchResult(BaseModel): - """Unified search result containing decisions from either system.""" - decision_type: Literal["norm_denetimi", "bireysel_basvuru"] = Field(..., description="Type of decisions returned") - decisions: List[Dict[str, Any]] = Field(default_factory=list, description="Decision list (structure varies by type)") - total_records_found: int = Field(0, description="Total number of records found") - retrieved_page_number: int = Field(1, description="Page number that was retrieved") - -class AnayasaUnifiedDocumentMarkdown(BaseModel): - """Unified document model for both Norm Denetimi and Bireysel Başvuru.""" - decision_type: Literal["norm_denetimi", "bireysel_basvuru"] = Field(..., description="Type of document") - source_url: HttpUrl = Field(..., description="Source URL of the document") - document_data: Dict[str, Any] = Field(default_factory=dict, description="Document content and metadata") - markdown_chunk: Optional[str] = Field(None, description="Markdown content chunk") - current_page: int = Field(1, description="Current page number") - total_pages: int = Field(1, description="Total number of pages") - is_paginated: bool = Field(False, description="Whether document is paginated") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/unified_client.py b/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/unified_client.py deleted file mode 100644 index 1319b85..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/anayasa_mcp_module/unified_client.py +++ /dev/null @@ -1,122 +0,0 @@ -# anayasa_mcp_module/unified_client.py -# Unified client for both Norm Denetimi and Bireysel Başvuru - -import logging -from typing import Optional -from urllib.parse import urlparse - -from .models import ( - AnayasaUnifiedSearchRequest, - AnayasaUnifiedSearchResult, - AnayasaUnifiedDocumentMarkdown, - # Removed AnayasaDecisionTypeEnum - now using string literals - AnayasaNormDenetimiSearchRequest, - AnayasaBireyselReportSearchRequest -) -from .client import AnayasaMahkemesiApiClient -from .bireysel_client import AnayasaBireyselBasvuruApiClient - -logger = logging.getLogger(__name__) - -class AnayasaUnifiedClient: - """Unified client that handles both Norm Denetimi and Bireysel Başvuru searches.""" - - def __init__(self, request_timeout: float = 60.0): - self.norm_client = AnayasaMahkemesiApiClient(request_timeout) - self.bireysel_client = AnayasaBireyselBasvuruApiClient(request_timeout) - - async def search_unified(self, params: AnayasaUnifiedSearchRequest) -> AnayasaUnifiedSearchResult: - """Unified search that routes to appropriate client based on decision_type.""" - - if params.decision_type == "norm_denetimi": - # Convert to norm denetimi request - norm_params = AnayasaNormDenetimiSearchRequest( - keywords_all=params.keywords_all or params.keywords, - keywords_any=params.keywords_any, - application_type=params.decision_type_norm, - page_to_fetch=params.page_to_fetch, - results_per_page=params.results_per_page - ) - - result = await self.norm_client.search_norm_denetimi_decisions(norm_params) - - # Convert to unified format - decisions_list = [decision.model_dump() for decision in result.decisions] - - return AnayasaUnifiedSearchResult( - decision_type="norm_denetimi", - decisions=decisions_list, - total_records_found=result.total_records_found, - retrieved_page_number=result.retrieved_page_number - ) - - elif params.decision_type == "bireysel_basvuru": - # Convert to bireysel başvuru request - bireysel_params = AnayasaBireyselReportSearchRequest( - keywords=params.keywords, - decision_start_date=params.decision_start_date, - decision_end_date=params.decision_end_date, - norm_type=params.norm_type, - subject_category=params.subject_category, - page_to_fetch=params.page_to_fetch, - results_per_page=params.results_per_page - ) - - result = await self.bireysel_client.search_bireysel_basvuru_report(bireysel_params) - - # Convert to unified format - decisions_list = [decision.model_dump() for decision in result.decisions] - - return AnayasaUnifiedSearchResult( - decision_type="bireysel_basvuru", - decisions=decisions_list, - total_records_found=result.total_records_found, - retrieved_page_number=result.retrieved_page_number - ) - - else: - raise ValueError(f"Unsupported decision type: {params.decision_type}") - - async def get_document_unified(self, document_url: str, page_number: int = 1) -> AnayasaUnifiedDocumentMarkdown: - """Unified document retrieval that auto-detects the appropriate client.""" - - # Auto-detect decision type based on URL - parsed_url = urlparse(document_url) - - if "normkararlarbilgibankasi" in parsed_url.netloc or "/ND/" in document_url: - # Norm Denetimi document - result = await self.norm_client.get_decision_document_as_markdown(document_url, page_number) - - return AnayasaUnifiedDocumentMarkdown( - decision_type="norm_denetimi", - source_url=result.source_url, - document_data=result.model_dump(), - markdown_chunk=result.markdown_chunk, - current_page=result.current_page, - total_pages=result.total_pages, - is_paginated=result.is_paginated - ) - - elif "kararlarbilgibankasi" in parsed_url.netloc or "/BB/" in document_url: - # Bireysel Başvuru document - result = await self.bireysel_client.get_decision_document_as_markdown(document_url, page_number) - - return AnayasaUnifiedDocumentMarkdown( - decision_type="bireysel_basvuru", - source_url=result.source_url, - document_data=result.model_dump(), - markdown_chunk=result.markdown_chunk, - current_page=result.current_page, - total_pages=result.total_pages, - is_paginated=result.is_paginated - ) - - else: - raise ValueError(f"Cannot determine document type from URL: {document_url}") - - async def close_client_session(self): - """Close both client sessions.""" - if hasattr(self.norm_client, 'close_client_session'): - await self.norm_client.close_client_session() - if hasattr(self.bireysel_client, 'close_client_session'): - await self.bireysel_client.close_client_session() \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/asgi_app.py b/saidsurucu-yargi-mcp-f5fa007/asgi_app.py deleted file mode 100644 index ba5a1f4..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/asgi_app.py +++ /dev/null @@ -1,566 +0,0 @@ -""" -ASGI application for Yargı MCP Server - -This module provides ASGI/HTTP access to the Yargı MCP server, -allowing it to be deployed as a web service with FastAPI wrapper -for Stripe webhook integration. - -Usage: - uvicorn asgi_app:app --host 0.0.0.0 --port 8000 -""" - -import os -import time -import logging -from datetime import datetime, timedelta -from fastapi import FastAPI, Request, HTTPException, Query -from fastapi.responses import JSONResponse, HTMLResponse -from fastapi.exception_handlers import http_exception_handler -from starlette.middleware import Middleware -from starlette.middleware.cors import CORSMiddleware -from starlette.responses import Response -from starlette.requests import Request as StarletteRequest - -# Import the MCP app creator function -from mcp_server_main import create_app - -# Import Stripe webhook router -from stripe_webhook import router as stripe_router - -# Import simplified MCP Auth HTTP adapter -from mcp_auth_http_simple import router as mcp_auth_router - -# OAuth configuration from environment variables -CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://accounts.yargimcp.com") -BASE_URL = os.getenv("BASE_URL", "https://yargimcp.com") - -# Setup logging -logger = logging.getLogger(__name__) - -# Configure CORS and Auth middleware -cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",") - -# Import FastMCP Bearer Auth Provider -from fastmcp.server.auth import BearerAuthProvider -from fastmcp.server.auth.providers.bearer import RSAKeyPair - -# Clerk JWT configuration for Bearer token validation -CLERK_SECRET_KEY = os.getenv("CLERK_SECRET_KEY") -CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://accounts.yargimcp.com") -CLERK_PUBLISHABLE_KEY = os.getenv("CLERK_PUBLISHABLE_KEY") - -# Configure Bearer token authentication -bearer_auth = None -if CLERK_SECRET_KEY and CLERK_ISSUER: - # Production: Use Clerk JWKS endpoint for token validation - bearer_auth = BearerAuthProvider( - jwks_uri=f"{CLERK_ISSUER}/.well-known/jwks.json", - issuer=CLERK_ISSUER, - algorithm="RS256", - audience=None, # Disable audience validation - Clerk uses different audience format - required_scopes=[] # Disable scope validation - Clerk JWT has ['read', 'search'] - ) - logger.info(f"Bearer auth configured with Clerk JWKS: {CLERK_ISSUER}/.well-known/jwks.json") -else: - # Development: Generate RSA key pair for testing - logger.warning("No Clerk credentials found - using development RSA key pair") - dev_key_pair = RSAKeyPair.generate() - bearer_auth = BearerAuthProvider( - public_key=dev_key_pair.public_key, - issuer="https://dev.yargimcp.com", - audience="dev-mcp-server", - required_scopes=["yargi.read"] - ) - - # Generate a test token for development - dev_token = dev_key_pair.create_token( - subject="dev-user", - issuer="https://dev.yargimcp.com", - audience="dev-mcp-server", - scopes=["yargi.read", "yargi.search"], - expires_in_seconds=3600 * 24 # 24 hours for development - ) - logger.info(f"Development Bearer token: {dev_token}") - -custom_middleware = [ - Middleware( - CORSMiddleware, - allow_origins=cors_origins, - allow_credentials=True, - allow_methods=["GET", "POST", "OPTIONS", "DELETE"], - allow_headers=["Content-Type", "Authorization", "X-Request-ID", "X-Session-ID"], - ), -] - -# Create MCP app with Bearer authentication -mcp_server = create_app(auth=bearer_auth) - -# Add Starlette middleware to FastAPI (not MCP) -# MCP already has Bearer auth, no need for additional middleware on MCP level - -# Create MCP Starlette sub-application with root path - mount will add /mcp prefix -mcp_app = mcp_server.http_app(path="/") - -# Configure JSON encoder for proper Turkish character support -import json -from fastapi.responses import JSONResponse - -class UTF8JSONResponse(JSONResponse): - def __init__(self, content=None, status_code=200, headers=None, **kwargs): - if headers is None: - headers = {} - headers["Content-Type"] = "application/json; charset=utf-8" - super().__init__(content, status_code, headers, **kwargs) - - def render(self, content) -> bytes: - return json.dumps( - content, - ensure_ascii=False, - allow_nan=False, - indent=None, - separators=(",", ":"), - ).encode("utf-8") - -# Create FastAPI wrapper application -app = FastAPI( - title="Yargı MCP Server", - description="MCP server for Turkish legal databases with OAuth authentication", - version="0.1.0", - middleware=custom_middleware, - default_response_class=UTF8JSONResponse # Use UTF-8 JSON encoder -) - -# Add Stripe webhook router to FastAPI -app.include_router(stripe_router, prefix="/api") - -# Add MCP Auth HTTP adapter to FastAPI (handles OAuth endpoints) -app.include_router(mcp_auth_router) - -# Custom 401 exception handler for MCP spec compliance -@app.exception_handler(401) -async def custom_401_handler(request: Request, exc: HTTPException): - """Custom 401 handler that adds WWW-Authenticate header as required by MCP spec""" - response = await http_exception_handler(request, exc) - - # Add WWW-Authenticate header pointing to protected resource metadata - # as required by RFC 9728 Section 5.1 and MCP Authorization spec - response.headers["WWW-Authenticate"] = ( - 'Bearer ' - 'error="invalid_token", ' - 'error_description="The access token is missing or invalid", ' - f'resource="{BASE_URL}/.well-known/oauth-protected-resource"' - ) - - return response - -# FastAPI health check endpoint - BEFORE mounting MCP app -@app.get("/health") -async def health_check(): - """Health check endpoint for monitoring""" - return JSONResponse({ - "status": "healthy", - "service": "Yargı MCP Server", - "version": "0.1.0", - "tools_count": len(mcp_server._tool_manager._tools), - "auth_enabled": os.getenv("ENABLE_AUTH", "false").lower() == "true" - }) - -# Add explicit redirect for /mcp to /mcp/ with method preservation -@app.api_route("/mcp", methods=["GET", "POST", "HEAD", "OPTIONS"]) -async def redirect_to_slash(request: Request): - """Redirect /mcp to /mcp/ preserving HTTP method with 308""" - from fastapi.responses import RedirectResponse - return RedirectResponse(url="/mcp/", status_code=308) - -# Mount MCP app at /mcp/ with trailing slash -app.mount("/mcp/", mcp_app) - -# Set the lifespan context after mounting -app.router.lifespan_context = mcp_app.lifespan - - -# SSE transport deprecated - removed - -# FastAPI root endpoint -@app.get("/") -async def root(): - """Root endpoint with service information""" - return JSONResponse({ - "service": "Yargı MCP Server", - "description": "MCP server for Turkish legal databases with OAuth authentication", - "endpoints": { - "mcp": "/mcp", - "health": "/health", - "status": "/status", - "stripe_webhook": "/api/stripe/webhook", - "oauth_login": "/auth/login", - "oauth_callback": "/auth/callback", - "oauth_google": "/auth/google/login", - "user_info": "/auth/user" - }, - "transports": { - "http": "/mcp" - }, - "supported_databases": [ - "Yargıtay (Court of Cassation)", - "Danıştay (Council of State)", - "Emsal (Precedent)", - "Uyuşmazlık Mahkemesi (Court of Jurisdictional Disputes)", - "Anayasa Mahkemesi (Constitutional Court)", - "Kamu İhale Kurulu (Public Procurement Authority)", - "Rekabet Kurumu (Competition Authority)", - "Sayıştay (Court of Accounts)", - "Bedesten API (Multiple courts)" - ], - "authentication": { - "enabled": os.getenv("ENABLE_AUTH", "false").lower() == "true", - "type": "OAuth 2.0 via Clerk", - "issuer": os.getenv("CLERK_ISSUER", "https://clerk.accounts.dev"), - "providers": ["google"], - "flow": "authorization_code" - } - }) - -# OAuth 2.0 Authorization Server Metadata proxy (for MCP clients that can't reach Clerk directly) -# MCP Auth Toolkit expects this to be under /mcp/.well-known/oauth-authorization-server -@app.get("/mcp/.well-known/oauth-authorization-server") -async def oauth_authorization_server(): - """OAuth 2.0 Authorization Server Metadata proxy to Clerk - MCP Auth Toolkit standard location""" - return JSONResponse({ - "issuer": BASE_URL, - "authorization_endpoint": "https://yargimcp.com/mcp-callback", - "token_endpoint": f"{BASE_URL}/token", - "jwks_uri": f"{CLERK_ISSUER}/.well-known/jwks.json", - "response_types_supported": ["code"], - "grant_types_supported": ["authorization_code", "refresh_token"], - "token_endpoint_auth_methods_supported": ["client_secret_basic", "none"], - "scopes_supported": ["read", "search", "openid", "profile", "email"], - "subject_types_supported": ["public"], - "id_token_signing_alg_values_supported": ["RS256"], - "claims_supported": ["sub", "iss", "aud", "exp", "iat", "email", "name"], - "code_challenge_methods_supported": ["S256"], - "service_documentation": f"{BASE_URL}/mcp", - "registration_endpoint": f"{BASE_URL}/register", - "resource_documentation": f"{BASE_URL}/mcp" - }) - -# Claude AI MCP specific endpoint format -@app.get("/.well-known/oauth-authorization-server/mcp") -async def oauth_authorization_server_mcp_suffix(): - """OAuth 2.0 Authorization Server Metadata - Claude AI MCP specific format""" - return JSONResponse({ - "issuer": BASE_URL, - "authorization_endpoint": "https://yargimcp.com/mcp-callback", - "token_endpoint": f"{BASE_URL}/token", - "jwks_uri": f"{CLERK_ISSUER}/.well-known/jwks.json", - "response_types_supported": ["code"], - "grant_types_supported": ["authorization_code", "refresh_token"], - "token_endpoint_auth_methods_supported": ["client_secret_basic", "none"], - "scopes_supported": ["read", "search", "openid", "profile", "email"], - "subject_types_supported": ["public"], - "id_token_signing_alg_values_supported": ["RS256"], - "claims_supported": ["sub", "iss", "aud", "exp", "iat", "email", "name"], - "code_challenge_methods_supported": ["S256"], - "service_documentation": f"{BASE_URL}/mcp", - "registration_endpoint": f"{BASE_URL}/register", - "resource_documentation": f"{BASE_URL}/mcp" - }) - -@app.get("/.well-known/oauth-protected-resource/mcp") -async def oauth_protected_resource_mcp_suffix(): - """OAuth 2.0 Protected Resource Metadata - Claude AI MCP specific format""" - return JSONResponse({ - "resource": BASE_URL, - "authorization_servers": [ - BASE_URL - ], - "scopes_supported": ["read", "search"], - "bearer_methods_supported": ["header"], - "resource_documentation": f"{BASE_URL}/mcp", - "resource_policy_uri": f"{BASE_URL}/privacy" - }) - -# Keep root level for compatibility with some MCP clients -@app.get("/.well-known/oauth-authorization-server") -async def oauth_authorization_server_root(): - """OAuth 2.0 Authorization Server Metadata proxy to Clerk - root level for compatibility""" - return JSONResponse({ - "issuer": BASE_URL, - "authorization_endpoint": "https://yargimcp.com/mcp-callback", - "token_endpoint": f"{BASE_URL}/token", - "jwks_uri": f"{CLERK_ISSUER}/.well-known/jwks.json", - "response_types_supported": ["code"], - "grant_types_supported": ["authorization_code", "refresh_token"], - "token_endpoint_auth_methods_supported": ["client_secret_basic", "none"], - "scopes_supported": ["read", "search", "openid", "profile", "email"], - "subject_types_supported": ["public"], - "id_token_signing_alg_values_supported": ["RS256"], - "claims_supported": ["sub", "iss", "aud", "exp", "iat", "email", "name"], - "code_challenge_methods_supported": ["S256"], - "service_documentation": f"{BASE_URL}/mcp", - "registration_endpoint": f"{BASE_URL}/register", - "resource_documentation": f"{BASE_URL}/mcp" - }) - -# Note: GET /mcp is handled by the mounted MCP app itself -# This prevents 405 Method Not Allowed errors on POST requests - -# OAuth 2.0 Protected Resource Metadata (RFC 9728) - MCP Spec Required -@app.get("/.well-known/oauth-protected-resource") -async def oauth_protected_resource(): - """OAuth 2.0 Protected Resource Metadata as required by MCP spec""" - return JSONResponse({ - "resource": BASE_URL, - "authorization_servers": [ - BASE_URL - ], - "scopes_supported": ["read", "search"], - "bearer_methods_supported": ["header"], - "resource_documentation": f"{BASE_URL}/mcp", - "resource_policy_uri": f"{BASE_URL}/privacy" - }) - -# Standard well-known discovery endpoint -@app.get("/.well-known/mcp") -async def well_known_mcp(): - """Standard MCP discovery endpoint""" - return JSONResponse({ - "mcp_server": { - "name": "Yargı MCP Server", - "version": "0.1.0", - "endpoint": f"{BASE_URL}/mcp", - "authentication": { - "type": "oauth2", - "authorization_url": f"{BASE_URL}/auth/login", - "scopes": ["read", "search"] - }, - "capabilities": ["tools", "resources"], - "tools_count": len(mcp_server._tool_manager._tools) - } - }) - -# MCP Discovery endpoint for ChatGPT integration -@app.get("/mcp/discovery") -async def mcp_discovery(): - """MCP Discovery endpoint for ChatGPT and other MCP clients""" - return JSONResponse({ - "name": "Yargı MCP Server", - "description": "MCP server for Turkish legal databases", - "version": "0.1.0", - "protocol": "mcp", - "transport": "http", - "endpoint": "/mcp", - "authentication": { - "type": "oauth2", - "authorization_url": "/auth/login", - "token_url": "/auth/callback", - "scopes": ["read", "search"], - "provider": "clerk" - }, - "capabilities": { - "tools": True, - "resources": True, - "prompts": False - }, - "tools_count": len(mcp_server._tool_manager._tools), - "contact": { - "url": BASE_URL, - "email": "support@yargi-mcp.dev" - } - }) - -# FastAPI status endpoint -@app.get("/status") -async def status(): - """Status endpoint with detailed information""" - tools = [] - for tool in mcp_server._tool_manager._tools.values(): - tools.append({ - "name": tool.name, - "description": tool.description[:100] + "..." if len(tool.description) > 100 else tool.description - }) - - return JSONResponse({ - "status": "operational", - "tools": tools, - "total_tools": len(tools), - "transport": "streamable_http", - "architecture": "FastAPI wrapper + MCP Starlette sub-app", - "auth_status": "enabled" if os.getenv("ENABLE_AUTH", "false").lower() == "true" else "disabled" - }) - -# Note: JWT token validation is now handled entirely by Clerk -# All authentication flows use Clerk JWT tokens directly - -async def validate_clerk_session(request: Request, clerk_token: str = None) -> str: - """Validate Clerk session from cookies or JWT token and return user_id""" - logger.info(f"Validating Clerk session - token provided: {bool(clerk_token)}") - - try: - # Try to import Clerk SDK - from clerk_backend_api import Clerk - clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY")) - - # Try JWT token first (from URL parameter) - if clerk_token: - logger.info("Validating Clerk JWT token from URL parameter") - try: - # Extract session_id from JWT token and verify with Clerk - import jwt - decoded_token = jwt.decode(clerk_token, options={"verify_signature": False}) - session_id = decoded_token.get("sid") # Use standard JWT 'sid' claim - - if session_id: - # Verify with Clerk using session_id - session = clerk.sessions.verify(session_id=session_id, token=clerk_token) - user_id = session.user_id if session else None - - if user_id: - logger.info(f"JWT token validation successful - user_id: {user_id}") - return user_id - else: - logger.error("JWT token validation failed - no user_id in session") - else: - logger.error("No session_id found in JWT token") - except Exception as e: - logger.error(f"JWT token validation failed: {str(e)}") - # Fall through to cookie validation - - # Fallback to cookie validation - logger.info("Attempting cookie-based session validation") - clerk_session = request.cookies.get("__session") - if not clerk_session: - logger.error("No Clerk session cookie found") - raise HTTPException(status_code=401, detail="No Clerk session found") - - # Validate session with Clerk - session = clerk.sessions.verify_session(clerk_session) - logger.info(f"Cookie session validation successful - user_id: {session.user_id}") - return session.user_id - - except ImportError: - # Fallback for development without Clerk SDK - logger.warning("Clerk SDK not available - using development fallback") - return "dev_user_123" - except Exception as e: - logger.error(f"Session validation failed: {str(e)}") - raise HTTPException(status_code=401, detail=f"Session validation failed: {str(e)}") - -# MCP OAuth Callback Endpoint -@app.get("/auth/mcp-callback") -async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)): - """Handle OAuth callback for MCP token generation""" - logger.info(f"MCP OAuth callback - clerk_token provided: {bool(clerk_token)}") - - try: - # Validate Clerk session with JWT token support - user_id = await validate_clerk_session(request, clerk_token) - logger.info(f"User authenticated successfully - user_id: {user_id}") - - # Use the Clerk JWT token directly (no need to generate custom token) - logger.info("User authenticated successfully via Clerk") - - # Return success response - return HTMLResponse(f""" - - - MCP Connection Successful - - - -

✅ MCP Connection Successful!

-

Your Yargı MCP integration is now active.

-
- Authentication:
- Use your Clerk JWT token directly with Bearer authentication -
-

You can now close this window and return to your MCP client.

- - - - """) - - except HTTPException as e: - logger.error(f"MCP OAuth callback failed: {e.detail}") - return HTMLResponse(f""" - - - MCP Connection Failed - - - -

❌ MCP Connection Failed

-

{e.detail}

-
- Debug Info:
- Clerk Token: {'✅ Provided' if clerk_token else '❌ Missing'}
- Error: {e.detail}
- Status: {e.status_code} -
-

Please try again or contact support.

- Return to Sign In - - - """, status_code=e.status_code) - except Exception as e: - logger.error(f"Unexpected error in MCP OAuth callback: {str(e)}") - return HTMLResponse(f""" - - - MCP Connection Error - - - -

❌ Unexpected Error

-

An unexpected error occurred during authentication.

-

Error: {str(e)}

- Return to Sign In - - - """, status_code=500) - -# OAuth2 Token Endpoint - Now uses Clerk JWT tokens directly -@app.post("/auth/mcp-token") -async def mcp_token_endpoint(request: Request): - """OAuth2 token endpoint for MCP clients - returns Clerk JWT token info""" - try: - # Validate Clerk session - user_id = await validate_clerk_session(request) - - return JSONResponse({ - "message": "Use your Clerk JWT token directly with Bearer authentication", - "token_type": "Bearer", - "scope": "yargi.read", - "user_id": user_id, - "instructions": "Include 'Authorization: Bearer YOUR_CLERK_JWT_TOKEN' in your requests" - }) - except HTTPException as e: - return JSONResponse( - status_code=e.status_code, - content={"error": "invalid_request", "error_description": e.detail} - ) - -# Note: Only HTTP transport supported - SSE transport deprecated - -# Export for uvicorn -__all__ = ["app"] \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/__init__.py deleted file mode 100644 index 7a2195d..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# bddk_mcp_module/__init__.py - -from .client import BddkApiClient -from .models import ( - BddkSearchRequest, - BddkDecisionSummary, - BddkSearchResult, - BddkDocumentMarkdown -) - -__all__ = [ - "BddkApiClient", - "BddkSearchRequest", - "BddkDecisionSummary", - "BddkSearchResult", - "BddkDocumentMarkdown" -] \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/client.py deleted file mode 100644 index 158e082..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/client.py +++ /dev/null @@ -1,247 +0,0 @@ -# bddk_mcp_module/client.py - -import httpx -from typing import List, Optional, Dict, Any -import logging -import os -import re -import io -import math -from urllib.parse import urlparse -from markitdown import MarkItDown - -from .models import ( - BddkSearchRequest, - BddkDecisionSummary, - BddkSearchResult, - BddkDocumentMarkdown -) - -logger = logging.getLogger(__name__) -if not logger.hasHandlers(): - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' - ) - -class BddkApiClient: - """ - API client for searching and retrieving BDDK (Banking Regulation Authority) decisions - using Tavily Search API for discovery and direct HTTP requests for content retrieval. - """ - - TAVILY_API_URL = "https://api.tavily.com/search" - BDDK_BASE_URL = "https://www.bddk.org.tr" - DOCUMENT_URL_TEMPLATE = "https://www.bddk.org.tr/Mevzuat/DokumanGetir/{document_id}" - DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000 # Character limit per page - - def __init__(self, request_timeout: float = 60.0): - """Initialize the BDDK API client.""" - self.tavily_api_key = os.getenv("TAVILY_API_KEY") - if not self.tavily_api_key: - # Fallback to development token - self.tavily_api_key = "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" - logger.info("Using fallback Tavily API token (development token)") - else: - logger.info("Using Tavily API key from environment variable") - - self.http_client = httpx.AsyncClient( - headers={ - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" - }, - timeout=httpx.Timeout(request_timeout) - ) - self.markitdown = MarkItDown() - - async def close_client_session(self): - """Close the HTTP client session.""" - await self.http_client.aclose() - logger.info("BddkApiClient: HTTP client session closed.") - - def _extract_document_id(self, url: str) -> Optional[str]: - """Extract document ID from BDDK URL.""" - # Primary pattern: https://www.bddk.org.tr/Mevzuat/DokumanGetir/310 - match = re.search(r'/DokumanGetir/(\d+)', url) - if match: - return match.group(1) - - # Alternative patterns for different BDDK URL formats - # Pattern: /Liste/55 -> use as document ID - match = re.search(r'/Liste/(\d+)', url) - if match: - return match.group(1) - - # Pattern: /EkGetir/13?ekId=381 -> use ekId as document ID - match = re.search(r'ekId=(\d+)', url) - if match: - return match.group(1) - - return None - - async def search_decisions( - self, - request: BddkSearchRequest - ) -> BddkSearchResult: - """ - Search for BDDK decisions using Tavily API. - - Args: - request: Search request parameters - - Returns: - BddkSearchResult with matching decisions - """ - try: - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.tavily_api_key}" - } - - # Tavily API request - enhanced for BDDK decision documents - query = f"{request.keywords} \"Karar Sayısı\"" - payload = { - "query": query, - "country": "turkey", - "include_domains": ["https://www.bddk.org.tr/Mevzuat/DokumanGetir"], - "max_results": request.pageSize, - "search_depth": "advanced" - } - - # Calculate offset for pagination - if request.page > 1: - # Tavily doesn't have direct pagination, so we'll need to handle this - # For now, we'll just return empty for pages > 1 - logger.warning(f"Tavily API doesn't support pagination. Page {request.page} requested.") - - response = await self.http_client.post( - self.TAVILY_API_URL, - json=payload, - headers=headers - ) - response.raise_for_status() - - data = response.json() - - # Log raw Tavily response for debugging - logger.info(f"Tavily returned {len(data.get('results', []))} results") - - # Convert Tavily results to our format - decisions = [] - for result in data.get("results", []): - # Extract document ID from URL - url = result.get("url", "") - logger.debug(f"Processing URL: {url}") - doc_id = self._extract_document_id(url) - if doc_id: - decision = BddkDecisionSummary( - title=result.get("title", "").replace("[PDF] ", "").strip(), - document_id=doc_id, - content=result.get("content", "")[:500] # Limit content length - ) - decisions.append(decision) - logger.debug(f"Added decision: {decision.title} (ID: {doc_id})") - else: - logger.warning(f"Could not extract document ID from URL: {url}") - - return BddkSearchResult( - decisions=decisions, - total_results=len(data.get("results", [])), - page=request.page, - pageSize=request.pageSize - ) - - except httpx.HTTPStatusError as e: - logger.error(f"HTTP error searching BDDK decisions: {e}") - if e.response.status_code == 401: - raise Exception("Tavily API authentication failed. Check API key.") - raise Exception(f"Failed to search BDDK decisions: {str(e)}") - except Exception as e: - logger.error(f"Error searching BDDK decisions: {e}") - raise Exception(f"Failed to search BDDK decisions: {str(e)}") - - async def get_document_markdown( - self, - document_id: str, - page_number: int = 1 - ) -> BddkDocumentMarkdown: - """ - Retrieve a BDDK document and convert it to Markdown format. - - Args: - document_id: BDDK document ID (e.g., '310') - page_number: Page number for paginated content (1-indexed) - - Returns: - BddkDocumentMarkdown with paginated content - """ - try: - # Try different URL patterns for BDDK documents - potential_urls = [ - f"https://www.bddk.org.tr/Mevzuat/DokumanGetir/{document_id}", - f"https://www.bddk.org.tr/Mevzuat/Liste/{document_id}", - f"https://www.bddk.org.tr/KurumHakkinda/EkGetir/13?ekId={document_id}", - f"https://www.bddk.org.tr/KurumHakkinda/EkGetir/5?ekId={document_id}" - ] - - document_url = None - response = None - - # Try each URL pattern until one works - for url in potential_urls: - try: - logger.info(f"Trying BDDK document URL: {url}") - response = await self.http_client.get( - url, - follow_redirects=True - ) - response.raise_for_status() - document_url = url - break - except httpx.HTTPStatusError: - continue - - if not response or not document_url: - raise Exception(f"Could not find document with ID {document_id}") - - logger.info(f"Successfully fetched BDDK document from: {document_url}") - - # Determine content type - content_type = response.headers.get("content-type", "").lower() - - # Convert to Markdown based on content type - if "pdf" in content_type: - # Handle PDF documents - pdf_stream = io.BytesIO(response.content) - result = self.markitdown.convert_stream(pdf_stream, file_extension=".pdf") - markdown_content = result.text_content - else: - # Handle HTML documents - html_stream = io.BytesIO(response.content) - result = self.markitdown.convert_stream(html_stream, file_extension=".html") - markdown_content = result.text_content - - # Clean up the markdown content - markdown_content = markdown_content.strip() - - # Calculate pagination - total_length = len(markdown_content) - total_pages = math.ceil(total_length / self.DOCUMENT_MARKDOWN_CHUNK_SIZE) - - # Extract the requested page - start_idx = (page_number - 1) * self.DOCUMENT_MARKDOWN_CHUNK_SIZE - end_idx = start_idx + self.DOCUMENT_MARKDOWN_CHUNK_SIZE - page_content = markdown_content[start_idx:end_idx] - - return BddkDocumentMarkdown( - document_id=document_id, - markdown_content=page_content, - page_number=page_number, - total_pages=total_pages - ) - - except httpx.HTTPStatusError as e: - logger.error(f"HTTP error fetching BDDK document {document_id}: {e}") - raise Exception(f"Failed to fetch BDDK document: {str(e)}") - except Exception as e: - logger.error(f"Error processing BDDK document {document_id}: {e}") - raise Exception(f"Failed to process BDDK document: {str(e)}") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/models.py deleted file mode 100644 index c945f26..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/bddk_mcp_module/models.py +++ /dev/null @@ -1,43 +0,0 @@ -# bddk_mcp_module/models.py - -from pydantic import BaseModel, Field -from typing import List, Optional - -class BddkSearchRequest(BaseModel): - """ - Request model for searching BDDK decisions via Tavily API. - - BDDK (Bankacılık Düzenleme ve Denetleme Kurumu) is Turkey's Banking - Regulation and Supervision Agency responsible for banking licenses, - electronic money institutions, and financial regulations. - """ - keywords: str = Field(..., description="Search keywords in Turkish") - page: int = Field(1, ge=1, description="Page number (1-indexed)") - pageSize: int = Field(10, ge=1, le=50, description="Results per page (1-50)") - -class BddkDecisionSummary(BaseModel): - """Summary of a BDDK decision from search results.""" - title: str = Field(..., description="Decision title") - document_id: str = Field(..., description="BDDK document ID (e.g., '310')") - content: str = Field(..., description="Decision summary/excerpt") - -class BddkSearchResult(BaseModel): - """Response model for BDDK decision search results.""" - decisions: List[BddkDecisionSummary] = Field( - default_factory=list, - description="List of matching BDDK decisions" - ) - total_results: int = Field(0, description="Total number of results") - page: int = Field(1, description="Current page number") - pageSize: int = Field(10, description="Results per page") - -class BddkDocumentMarkdown(BaseModel): - """ - BDDK decision document converted to Markdown format. - - Supports paginated content for long documents (5000 chars per page). - """ - document_id: str = Field(..., description="BDDK document ID") - markdown_content: str = Field("", description="Document content in Markdown") - page_number: int = Field(1, description="Current page number") - total_pages: int = Field(1, description="Total number of pages") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/__init__.py deleted file mode 100644 index 90b95d3..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# bedesten_mcp_module/__init__.py \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/client.py deleted file mode 100644 index ad6f1a8..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/client.py +++ /dev/null @@ -1,181 +0,0 @@ -# bedesten_mcp_module/client.py - -import httpx -import base64 -from typing import Optional -import logging -from markitdown import MarkItDown -import io - -from .models import ( - BedestenSearchRequest, BedestenSearchResponse, - BedestenDocumentRequest, BedestenDocumentResponse, - BedestenDocumentMarkdown, BedestenDocumentRequestData -) -from .enums import get_full_birim_adi - -logger = logging.getLogger(__name__) - -class BedestenApiClient: - """ - API Client for Bedesten (bedesten.adalet.gov.tr) - Alternative legal decision search system. - Currently used for Yargıtay decisions, but can be extended for other court types. - """ - BASE_URL = "https://bedesten.adalet.gov.tr" - SEARCH_ENDPOINT = "/emsal-karar/searchDocuments" - DOCUMENT_ENDPOINT = "/emsal-karar/getDocumentContent" - - def __init__(self, request_timeout: float = 60.0): - self.http_client = httpx.AsyncClient( - base_url=self.BASE_URL, - headers={ - "Accept": "*/*", - "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7", - "AdaletApplicationName": "UyapMevzuat", - "Content-Type": "application/json; charset=utf-8", - "Origin": "https://mevzuat.adalet.gov.tr", - "Referer": "https://mevzuat.adalet.gov.tr/", - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-site", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36" - }, - timeout=request_timeout - ) - - async def search_documents(self, search_request: BedestenSearchRequest) -> BedestenSearchResponse: - """ - Search for documents using Bedesten API. - Currently supports: YARGITAYKARARI, DANISTAYKARARI, YERELHUKMAHKARARI, etc. - """ - logger.info(f"BedestenApiClient: Searching documents with phrase: {search_request.data.phrase}") - - # Map abbreviated birimAdi to full Turkish name before sending to API - original_birim_adi = search_request.data.birimAdi - mapped_birim_adi = get_full_birim_adi(original_birim_adi) - search_request.data.birimAdi = mapped_birim_adi - if original_birim_adi != "ALL": - logger.info(f"BedestenApiClient: Mapped birimAdi '{original_birim_adi}' to '{mapped_birim_adi}'") - - try: - # Create request dict and remove birimAdi if empty - request_dict = search_request.model_dump() - if not request_dict["data"]["birimAdi"]: # Remove if empty string - del request_dict["data"]["birimAdi"] - - response = await self.http_client.post( - self.SEARCH_ENDPOINT, - json=request_dict - ) - response.raise_for_status() - response_json = response.json() - - # Parse and return the response - return BedestenSearchResponse(**response_json) - - except httpx.RequestError as e: - logger.error(f"BedestenApiClient: HTTP request error during search: {e}") - raise - except Exception as e: - logger.error(f"BedestenApiClient: Error processing search response: {e}") - raise - - async def get_document_as_markdown(self, document_id: str) -> BedestenDocumentMarkdown: - """ - Get document content and convert to markdown. - Handles both HTML (text/html) and PDF (application/pdf) content types. - """ - logger.info(f"BedestenApiClient: Fetching document for markdown conversion (ID: {document_id})") - - try: - # Prepare request - doc_request = BedestenDocumentRequest( - data=BedestenDocumentRequestData(documentId=document_id) - ) - - # Get document - response = await self.http_client.post( - self.DOCUMENT_ENDPOINT, - json=doc_request.model_dump() - ) - response.raise_for_status() - response_json = response.json() - doc_response = BedestenDocumentResponse(**response_json) - - # Decode base64 content - content_bytes = base64.b64decode(doc_response.data.content) - mime_type = doc_response.data.mimeType - - logger.info(f"BedestenApiClient: Document mime type: {mime_type}") - - # Convert to markdown based on mime type - if mime_type == "text/html": - html_content = content_bytes.decode('utf-8') - markdown_content = self._convert_html_to_markdown(html_content) - elif mime_type == "application/pdf": - markdown_content = self._convert_pdf_to_markdown(content_bytes) - else: - logger.warning(f"Unsupported mime type: {mime_type}") - markdown_content = f"Unsupported content type: {mime_type}. Unable to convert to markdown." - - return BedestenDocumentMarkdown( - documentId=document_id, - markdown_content=markdown_content, - source_url=f"{self.BASE_URL}/document/{document_id}", - mime_type=mime_type - ) - - except httpx.RequestError as e: - logger.error(f"BedestenApiClient: HTTP error fetching document {document_id}: {e}") - raise - except Exception as e: - logger.error(f"BedestenApiClient: Error processing document {document_id}: {e}") - raise - - def _convert_html_to_markdown(self, html_content: str) -> Optional[str]: - """Convert HTML to Markdown using MarkItDown""" - if not html_content: - return None - - try: - # Convert HTML string to bytes and create BytesIO stream - html_bytes = html_content.encode('utf-8') - html_stream = io.BytesIO(html_bytes) - - # Pass BytesIO stream to MarkItDown to avoid temp file creation - md_converter = MarkItDown() - result = md_converter.convert(html_stream) - markdown_content = result.text_content - - logger.info("Successfully converted HTML to Markdown") - return markdown_content - - except Exception as e: - logger.error(f"Error converting HTML to Markdown: {e}") - return f"Error converting HTML content: {str(e)}" - - def _convert_pdf_to_markdown(self, pdf_bytes: bytes) -> Optional[str]: - """Convert PDF to Markdown using MarkItDown""" - if not pdf_bytes: - return None - - try: - # Create BytesIO stream from PDF bytes - pdf_stream = io.BytesIO(pdf_bytes) - - # Pass BytesIO stream to MarkItDown to avoid temp file creation - md_converter = MarkItDown() - result = md_converter.convert(pdf_stream) - markdown_content = result.text_content - - logger.info("Successfully converted PDF to Markdown") - return markdown_content - - except Exception as e: - logger.error(f"Error converting PDF to Markdown: {e}") - return f"Error converting PDF content: {str(e)}. The document may be corrupted or in an unsupported format." - - async def close_client_session(self): - """Close HTTP client session""" - await self.http_client.aclose() - logger.info("BedestenApiClient: HTTP client session closed.") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/enums.py b/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/enums.py deleted file mode 100644 index 7cef054..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/enums.py +++ /dev/null @@ -1,113 +0,0 @@ -# bedesten_mcp_module/enums.py - -from typing import Literal - -# Unified compressed enum for both Yargıtay and Danıştay chambers -BirimAdiEnum = Literal[ - "ALL", # All chambers - - # Yargıtay (Court of Cassation) - Civil Chambers - "H1", "H2", "H3", "H4", "H5", "H6", "H7", "H8", "H9", "H10", - "H11", "H12", "H13", "H14", "H15", "H16", "H17", "H18", "H19", "H20", - "H21", "H22", "H23", - - # Yargıtay - Criminal Chambers - "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9", "C10", - "C11", "C12", "C13", "C14", "C15", "C16", "C17", "C18", "C19", "C20", - "C21", "C22", "C23", - - # Yargıtay - Councils and Assemblies - "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 - - # Danıştay (Council of State) - Chambers - "D1", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10", - "D11", "D12", "D13", "D14", "D15", "D16", "D17", - - # Danıştay - Councils and Boards - "DBGK", # Büyük Gen.Kur. (Grand General Assembly) - "IDDK", # İdare Dava Daireleri Kurulu - "VDDK", # Vergi Dava Daireleri Kurulu - "IBK", # İçtihatları Birleştirme Kurulu - "IIK", # İdari İşler Kurulu - "DBK", # Başkanlar Kurulu - - # Military High Administrative Court - "AYIM", # Askeri Yüksek İdare Mahkemesi - "AYIMDK", # Askeri Yüksek İdare Mahkemesi Daireler Kurulu - "AYIMB", # Askeri Yüksek İdare Mahkemesi Başsavcılığı - "AYIM1", # Askeri Yüksek İdare Mahkemesi 1. Daire - "AYIM2", # Askeri Yüksek İdare Mahkemesi 2. Daire - "AYIM3" # Askeri Yüksek İdare Mahkemesi 3. Daire -] - -# Mapping from abbreviated values to full Turkish API values -BIRIM_ADI_MAPPING = { - "ALL": None, # Will be handled specially in client - - # Yargıtay Civil Chambers (1-23) - "H1": "1. Hukuk Dairesi", "H2": "2. Hukuk Dairesi", "H3": "3. Hukuk Dairesi", - "H4": "4. Hukuk Dairesi", "H5": "5. Hukuk Dairesi", "H6": "6. Hukuk Dairesi", - "H7": "7. Hukuk Dairesi", "H8": "8. Hukuk Dairesi", "H9": "9. Hukuk Dairesi", - "H10": "10. Hukuk Dairesi", "H11": "11. Hukuk Dairesi", "H12": "12. Hukuk Dairesi", - "H13": "13. Hukuk Dairesi", "H14": "14. Hukuk Dairesi", "H15": "15. Hukuk Dairesi", - "H16": "16. Hukuk Dairesi", "H17": "17. Hukuk Dairesi", "H18": "18. Hukuk Dairesi", - "H19": "19. Hukuk Dairesi", "H20": "20. Hukuk Dairesi", "H21": "21. Hukuk Dairesi", - "H22": "22. Hukuk Dairesi", "H23": "23. Hukuk Dairesi", - - # Yargıtay Criminal Chambers (1-23) - "C1": "1. Ceza Dairesi", "C2": "2. Ceza Dairesi", "C3": "3. Ceza Dairesi", - "C4": "4. Ceza Dairesi", "C5": "5. Ceza Dairesi", "C6": "6. Ceza Dairesi", - "C7": "7. Ceza Dairesi", "C8": "8. Ceza Dairesi", "C9": "9. Ceza Dairesi", - "C10": "10. Ceza Dairesi", "C11": "11. Ceza Dairesi", "C12": "12. Ceza Dairesi", - "C13": "13. Ceza Dairesi", "C14": "14. Ceza Dairesi", "C15": "15. Ceza Dairesi", - "C16": "16. Ceza Dairesi", "C17": "17. Ceza Dairesi", "C18": "18. Ceza Dairesi", - "C19": "19. Ceza Dairesi", "C20": "20. Ceza Dairesi", "C21": "21. Ceza Dairesi", - "C22": "22. Ceza Dairesi", "C23": "23. Ceza Dairesi", - - # Yargıtay Councils and Assemblies - "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", - - # Danıştay Chambers (1-17) - "D1": "1. Daire", "D2": "2. Daire", "D3": "3. Daire", "D4": "4. Daire", - "D5": "5. Daire", "D6": "6. Daire", "D7": "7. Daire", "D8": "8. Daire", - "D9": "9. Daire", "D10": "10. Daire", "D11": "11. Daire", "D12": "12. Daire", - "D13": "13. Daire", "D14": "14. Daire", "D15": "15. Daire", "D16": "16. Daire", - "D17": "17. Daire", - - # Danıştay Councils and Boards - "DBGK": "Büyük Gen.Kur.", - "IDDK": "İdare Dava Daireleri Kurulu", - "VDDK": "Vergi Dava Daireleri Kurulu", - "IBK": "İçtihatları Birleştirme Kurulu", - "IIK": "İdari İşler Kurulu", - "DBK": "Başkanlar Kurulu", - - # Military High Administrative Court - "AYIM": "Askeri Yüksek İdare Mahkemesi", - "AYIMDK": "Askeri Yüksek İdare Mahkemesi Daireler Kurulu", - "AYIMB": "Askeri Yüksek İdare Mahkemesi Başsavcılığı", - "AYIM1": "Askeri Yüksek İdare Mahkemesi 1. Daire", - "AYIM2": "Askeri Yüksek İdare Mahkemesi 2. Daire", - "AYIM3": "Askeri Yüksek İdare Mahkemesi 3. Daire" -} - -# Helper function to get full Turkish name from abbreviated value -def get_full_birim_adi(abbreviated_value: str) -> str: - """Convert abbreviated birimAdi value to full Turkish name for API calls.""" - if abbreviated_value == "ALL" or not abbreviated_value: - return "" # Empty string for ALL or None - - return BIRIM_ADI_MAPPING.get(abbreviated_value, abbreviated_value) - -# Helper function to validate abbreviated value -def is_valid_birim_adi(abbreviated_value: str) -> bool: - """Check if abbreviated birimAdi value is valid.""" - return abbreviated_value in BIRIM_ADI_MAPPING \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/models.py deleted file mode 100644 index 6c6c24d..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/bedesten_mcp_module/models.py +++ /dev/null @@ -1,91 +0,0 @@ -# bedesten_mcp_module/models.py - -from pydantic import BaseModel, Field -from typing import List, Optional, Dict, Any, Literal, Union -from datetime import datetime - -# Import compressed BirimAdiEnum for chamber filtering -from .enums import BirimAdiEnum - -# Court Type Options for Unified Search -BedestenCourtTypeEnum = Literal[ - "YARGITAYKARARI", # Yargıtay (Court of Cassation) - "DANISTAYKARAR", # Danıştay (Council of State) - "YERELHUKUK", # Local Civil Courts - "ISTINAFHUKUK", # Civil Courts of Appeals - "KYB" # Extraordinary Appeals (Kanun Yararına Bozma) -] - -# Search Request Models -class BedestenSearchData(BaseModel): - pageSize: int = Field(..., description="Results per page (1-10)") - pageNumber: int = Field(..., description="Page number (1-indexed)") - itemTypeList: List[str] = Field(..., description="Court type filter (YARGITAYKARARI/DANISTAYKARAR/YERELHUKUK/ISTINAFHUKUK/KYB)") - phrase: str = Field(..., description="Search phrase. Supports: 'word', \"exact phrase\", +required, -exclude, AND/OR/NOT operators. No wildcards or regex.") - 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) - • Danıştay: D1-D17 (1-17. Daire), DBGK (Büyük Gen.Kur.), IDDK (İdare Dava Daireleri Kurulu), VDDK (Vergi Dava Daireleri Kurulu), IBK (İçtihatları Birleştirme Kurulu), IIK (İdari İşler Kurulu), DBK (Başkanlar Kurulu), AYIM (Askeri Yüksek İdare Mahkemesi), AYIM1-3 (Askeri Yüksek İdare Mahkemesi 1-3. Daire) - """) - kararTarihiStart: Optional[str] = Field(None, description="Start date (ISO 8601 format)") - kararTarihiEnd: Optional[str] = Field(None, description="End date (ISO 8601 format)") - sortFields: List[str] = Field(default=["KARAR_TARIHI"], description="Sort fields") - sortDirection: str = Field(default="desc", description="Sort direction (asc/desc)") - -class BedestenSearchRequest(BaseModel): - data: BedestenSearchData - applicationName: str = "UyapMevzuat" - paging: bool = True - -# Search Response Models -class BedestenItemType(BaseModel): - name: str - description: str - -class BedestenDecisionEntry(BaseModel): - documentId: str - itemType: BedestenItemType - birimId: Optional[str] = None - birimAdi: Optional[str] - esasNoYil: Optional[int] = None - esasNoSira: Optional[int] = None - kararNoYil: Optional[int] = None - kararNoSira: Optional[int] = None - kararTuru: Optional[str] = None - kararTarihi: str - kararTarihiStr: str - kesinlesmeDurumu: Optional[str] = None - kararNo: Optional[str] = None - esasNo: Optional[str] = None - -class BedestenSearchDataResponse(BaseModel): - emsalKararList: List[BedestenDecisionEntry] - total: int - start: int - -class BedestenSearchResponse(BaseModel): - data: Optional[BedestenSearchDataResponse] - metadata: Dict[str, Any] - -# Document Request/Response Models -class BedestenDocumentRequestData(BaseModel): - documentId: str - -class BedestenDocumentRequest(BaseModel): - data: BedestenDocumentRequestData - applicationName: str = "UyapMevzuat" - -class BedestenDocumentData(BaseModel): - content: str # Base64 encoded HTML or PDF - mimeType: str - version: int - -class BedestenDocumentResponse(BaseModel): - data: BedestenDocumentData - metadata: Dict[str, Any] - -class BedestenDocumentMarkdown(BaseModel): - documentId: str = Field(..., description="The document ID (Belge Kimliği) from Bedesten") - markdown_content: Optional[str] = Field(None, description="The decision content (Karar İçeriği) converted to Markdown") - source_url: str = Field(..., description="The source URL (Kaynak URL) of the document") - mime_type: Optional[str] = Field(None, description="Original content type (İçerik Türü) (text/html or application/pdf)") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/check_response_format.py b/saidsurucu-yargi-mcp-f5fa007/check_response_format.py deleted file mode 100644 index 1e814c4..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/check_response_format.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -from fastmcp import Client -from mcp_server_main import app -import json -import asyncio - -async def check_response_format(): - client = Client(app) - async with client: - result = await client.call_tool('search_bedesten_unified', { - 'phrase': 'mülkiyet', - 'court_types': ['YARGITAYKARARI'], - 'birimAdi': 'H1', - 'pageSize': 3 - }) - if result and result.content: - data = json.loads(result.content[0].text) - print('Response keys:', list(data.keys())) - print('Sample response:', json.dumps(data, indent=2, ensure_ascii=False)[:500]) - -asyncio.run(check_response_format()) \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/client.py deleted file mode 100644 index 0a713ff..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/client.py +++ /dev/null @@ -1,192 +0,0 @@ -# danistay_mcp_module/client.py - -import httpx -from bs4 import BeautifulSoup -from typing import Dict, Any, List, Optional -import logging -import html -import re -import io -from markitdown import MarkItDown - -from .models import ( - DanistayKeywordSearchRequest, - DanistayDetailedSearchRequest, - DanistayApiResponse, - DanistayDocumentMarkdown, - DanistayKeywordSearchRequestData, - DanistayDetailedSearchRequestData -) - -logger = logging.getLogger(__name__) -if not logger.hasHandlers(): - logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - -class DanistayApiClient: - BASE_URL = "https://karararama.danistay.gov.tr" - KEYWORD_SEARCH_ENDPOINT = "/aramalist" - DETAILED_SEARCH_ENDPOINT = "/aramadetaylist" - DOCUMENT_ENDPOINT = "/getDokuman" - - def __init__(self, request_timeout: float = 30.0): - self.http_client = httpx.AsyncClient( - base_url=self.BASE_URL, - headers={ - "Content-Type": "application/json; charset=UTF-8", # Arama endpoint'leri için - "Accept": "application/json, text/plain, */*", # Arama endpoint'leri için - "X-Requested-With": "XMLHttpRequest", - }, - timeout=request_timeout, - verify=False - ) - - def _prepare_keywords_for_api(self, keywords: List[str]) -> List[str]: - return ['"' + k.strip('"') + '"' for k in keywords if k and k.strip()] - - async def search_keyword_decisions( - self, - params: DanistayKeywordSearchRequest - ) -> DanistayApiResponse: - data_for_payload = DanistayKeywordSearchRequestData( - andKelimeler=self._prepare_keywords_for_api(params.andKelimeler), - orKelimeler=self._prepare_keywords_for_api(params.orKelimeler), - notAndKelimeler=self._prepare_keywords_for_api(params.notAndKelimeler), - notOrKelimeler=self._prepare_keywords_for_api(params.notOrKelimeler), - pageSize=params.pageSize, - pageNumber=params.pageNumber - ) - final_payload = {"data": data_for_payload.model_dump(exclude_none=True)} - logger.info(f"DanistayApiClient: Performing KEYWORD search via {self.KEYWORD_SEARCH_ENDPOINT} with payload: {final_payload}") - return await self._execute_api_search(self.KEYWORD_SEARCH_ENDPOINT, final_payload) - - async def search_detailed_decisions( - self, - params: DanistayDetailedSearchRequest - ) -> DanistayApiResponse: - data_for_payload = DanistayDetailedSearchRequestData( - daire=params.daire or "", - esasYil=params.esasYil or "", - esasIlkSiraNo=params.esasIlkSiraNo or "", - esasSonSiraNo=params.esasSonSiraNo or "", - kararYil=params.kararYil or "", - kararIlkSiraNo=params.kararIlkSiraNo or "", - kararSonSiraNo=params.kararSonSiraNo or "", - baslangicTarihi=params.baslangicTarihi or "", - bitisTarihi=params.bitisTarihi or "", - mevzuatNumarasi=params.mevzuatNumarasi or "", - mevzuatAdi=params.mevzuatAdi or "", - madde=params.madde or "", - siralama="1", - siralamaDirection="desc", - pageSize=params.pageSize, - pageNumber=params.pageNumber - ) - # Create request dict and remove empty string fields to avoid API issues - payload_dict = data_for_payload.model_dump(exclude_defaults=False, exclude_none=False) - # Remove empty string fields that might cause API issues - cleaned_payload = {k: v for k, v in payload_dict.items() if v != ""} - final_payload = {"data": cleaned_payload} - logger.info(f"DanistayApiClient: Performing DETAILED search via {self.DETAILED_SEARCH_ENDPOINT} with payload: {final_payload}") - return await self._execute_api_search(self.DETAILED_SEARCH_ENDPOINT, final_payload) - - async def _execute_api_search(self, endpoint: str, payload: Dict) -> DanistayApiResponse: - try: - response = await self.http_client.post(endpoint, json=payload) - response.raise_for_status() - response_json_data = response.json() - logger.debug(f"DanistayApiClient: Raw API response from {endpoint}: {response_json_data}") - api_response_parsed = DanistayApiResponse(**response_json_data) - if api_response_parsed.data and api_response_parsed.data.data: - for decision_item in api_response_parsed.data.data: - if decision_item.id: - decision_item.document_url = f"{self.BASE_URL}{self.DOCUMENT_ENDPOINT}?id={decision_item.id}" - return api_response_parsed - except httpx.RequestError as e: - logger.error(f"DanistayApiClient: HTTP request error during search to {endpoint}: {e}") - raise - except Exception as e: - logger.error(f"DanistayApiClient: Error processing or validating search response from {endpoint}: {e}") - raise - - def _convert_html_to_markdown_danistay(self, direct_html_content: str) -> Optional[str]: - """ - Converts direct HTML content (assumed from Danıştay /getDokuman) to Markdown. - """ - if not direct_html_content: - return None - - # Basic HTML unescaping and fixing common escaped characters - # This step might be less critical if MarkItDown handles them, but good for pre-cleaning. - processed_html = html.unescape(direct_html_content) - processed_html = processed_html.replace('\\"', '"') # If any such JS-escaped strings exist - # Danistay HTML doesn't seem to have \\r\\n etc. from the example, but keeping for robustness - processed_html = processed_html.replace('\\r\\n', '\n').replace('\\n', '\n').replace('\\t', '\t') - - # For simplicity and to leverage MarkItDown's capability to handle full docs, - # we pass the pre-processed full HTML. - html_input_for_markdown = processed_html - - markdown_text = None - try: - # Convert HTML string to bytes and create BytesIO stream - html_bytes = html_input_for_markdown.encode('utf-8') - html_stream = io.BytesIO(html_bytes) - - # Pass BytesIO stream to MarkItDown to avoid temp file creation - md_converter = MarkItDown() - conversion_result = md_converter.convert(html_stream) - markdown_text = conversion_result.text_content - logger.info("DanistayApiClient: HTML to Markdown conversion successful.") - except Exception as e: - logger.error(f"DanistayApiClient: Error during MarkItDown HTML to Markdown conversion: {e}") - - return markdown_text - - async def get_decision_document_as_markdown(self, id: str) -> DanistayDocumentMarkdown: - """ - Retrieves a specific Danıştay decision by ID and returns its content as Markdown. - The /getDokuman endpoint for Danıştay requires arananKelime parameter. - """ - # Add required arananKelime parameter - using empty string as minimum requirement - document_api_url = f"{self.DOCUMENT_ENDPOINT}?id={id}&arananKelime=" - source_url = f"{self.BASE_URL}{document_api_url}" - logger.info(f"DanistayApiClient: Fetching Danistay document for Markdown (ID: {id}) from {source_url}") - - try: - # For direct HTML response, we might want different headers if the API is sensitive, - # but httpx usually handles basic GET requests well. - response = await self.http_client.get(document_api_url) - response.raise_for_status() - - # Danıştay /getDokuman directly returns HTML text - html_content_from_api = response.text - - if not isinstance(html_content_from_api, str) or not html_content_from_api.strip(): - logger.warning(f"DanistayApiClient: Received empty or non-string HTML content for ID {id}.") - # Return with None markdown_content if HTML is effectively empty - return DanistayDocumentMarkdown( - id=id, - markdown_content=None, - source_url=source_url - ) - - markdown_content = self._convert_html_to_markdown_danistay(html_content_from_api) - - return DanistayDocumentMarkdown( - id=id, - markdown_content=markdown_content, - source_url=source_url - ) - except httpx.RequestError as e: - logger.error(f"DanistayApiClient: HTTP error fetching Danistay document (ID: {id}): {e}") - raise - # Removed ValueError for JSON as Danistay /getDokuman returns direct HTML - except Exception as e: # Catches other errors like MarkItDown issues if they propagate - logger.error(f"DanistayApiClient: General error processing Danistay document (ID: {id}): {e}") - raise - - async def close_client_session(self): - """Closes the HTTPX client session.""" - if self.http_client and not self.http_client.is_closed: - await self.http_client.aclose() - logger.info("DanistayApiClient: HTTP client session closed.") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/models.py deleted file mode 100644 index 8c657a9..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/danistay_mcp_module/models.py +++ /dev/null @@ -1,112 +0,0 @@ -# danistay_mcp_module/models.py - -from pydantic import BaseModel, Field, HttpUrl, ConfigDict -from typing import List, Optional, Dict, Any - -class DanistayBaseSearchRequest(BaseModel): - """Base model for common search parameters for Danistay.""" - pageSize: int = Field(default=10, ge=1, le=10) - pageNumber: int = Field(default=1, ge=1) - # siralama and siralamaDirection are part of detailed search, not necessarily keyword search - # as per user's provided payloads. - -class DanistayKeywordSearchRequestData(BaseModel): - """Internal data model for the keyword search payload's 'data' field.""" - andKelimeler: List[str] = Field(default_factory=list) - orKelimeler: List[str] = Field(default_factory=list) - notAndKelimeler: List[str] = Field(default_factory=list) - notOrKelimeler: List[str] = Field(default_factory=list) - pageSize: int - pageNumber: int - -class DanistayKeywordSearchRequest(BaseModel): # This is the model the MCP tool will accept - """Model for keyword-based search request for Danistay.""" - andKelimeler: List[str] = Field(default_factory=list, description="AND keywords") - orKelimeler: List[str] = Field(default_factory=list, description="OR keywords") - notAndKelimeler: List[str] = Field(default_factory=list, description="NOT AND keywords") - notOrKelimeler: List[str] = Field(default_factory=list, description="NOT OR keywords") - pageSize: int = Field(default=10, ge=1, le=10) - pageNumber: int = Field(default=1, ge=1) - -class DanistayDetailedSearchRequestData(BaseModel): # Internal data model for detailed search payload - """Internal data model for the detailed search payload's 'data' field.""" - daire: Optional[str] = "" # API expects empty string for None - esasYil: Optional[str] = "" - esasIlkSiraNo: Optional[str] = "" - esasSonSiraNo: Optional[str] = "" - kararYil: Optional[str] = "" - kararIlkSiraNo: Optional[str] = "" - kararSonSiraNo: Optional[str] = "" - baslangicTarihi: Optional[str] = "" - bitisTarihi: Optional[str] = "" - mevzuatNumarasi: Optional[str] = "" - mevzuatAdi: Optional[str] = "" - madde: Optional[str] = "" - siralama: str # Seems mandatory in detailed search payload - siralamaDirection: str # Seems mandatory - pageSize: int - pageNumber: int - # Note: 'arananKelime' is not in the detailed search payload example provided by user. - # If it can be included, it should be added here. - -class DanistayDetailedSearchRequest(DanistayBaseSearchRequest): # MCP tool will accept this - """Model for detailed search request for Danistay.""" - daire: str = Field("", description="Chamber") - esasYil: str = Field("", description="Case year") - esasIlkSiraNo: str = Field("", description="Start case no") - esasSonSiraNo: str = Field("", description="End case no") - kararYil: str = Field("", description="Decision year") - kararIlkSiraNo: str = Field("", description="Start decision no") - kararSonSiraNo: str = Field("", description="End decision no") - baslangicTarihi: str = Field("", description="Start date") - bitisTarihi: str = Field("", description="End date") - mevzuatNumarasi: str = Field("", description="Law number") - mevzuatAdi: str = Field("", description="Law name") - madde: str = Field("", description="Article") - # Add a general keyword field if detailed search also supports it - # arananKelime: Optional[str] = Field(None, description="General keyword for detailed search.") - - -class DanistayApiDecisionEntry(BaseModel): - """Model for an individual decision entry from the Danistay API search response. - Based on user-provided response samples for both keyword and detailed search. - """ - id: str - # The API response for keyword search uses "daireKurul", detailed search example uses "daire". - # We use an alias to handle both and map to a consistent field name "chamber". - chamber: str = Field("", alias="daire", description="Chamber") - esasNo: str = Field("", description="Case number") - kararNo: str = Field("", description="Decision number") - kararTarihi: str = Field("", description="Decision date") - arananKelime: str = Field("", description="Keyword") - # index: Optional[int] = None # Present in response, can be added if needed by MCP tool - # siraNo: Optional[int] = None # Present in detailed response, can be added - - document_url: Optional[HttpUrl] = Field(None, description="Document URL") - - model_config = ConfigDict(populate_by_name=True, extra='ignore') # Important for alias to work and ignore extra fields - -class DanistayApiResponseInnerData(BaseModel): - """Model for the inner 'data' object in the Danistay API search response.""" - data: List[DanistayApiDecisionEntry] - recordsTotal: int - recordsFiltered: int - draw: int = Field(0, description="Draw counter") - -class DanistayApiResponse(BaseModel): - """Model for the complete search response from the Danistay API.""" - data: Optional[DanistayApiResponseInnerData] = Field(None, description="Response data, can be null when no results found") - metadata: Optional[Dict[str, Any]] = Field(None, description="Optional metadata (Meta Veri) from API.") - -class DanistayDocumentMarkdown(BaseModel): - """Model for a Danistay decision document, containing only Markdown content.""" - id: str - markdown_content: str = Field("", description="The decision content (Karar İçeriği) converted to Markdown.") - source_url: HttpUrl - -class CompactDanistaySearchResult(BaseModel): - """A compact search result model for the MCP tool to return.""" - decisions: List[DanistayApiDecisionEntry] - total_records: int - requested_page: int - page_size: int \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/docker-compose.yml b/saidsurucu-yargi-mcp-f5fa007/docker-compose.yml deleted file mode 100644 index d36eee7..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/docker-compose.yml +++ /dev/null @@ -1,66 +0,0 @@ -version: '3.8' - -services: - yargi-mcp: - build: . - image: yargi-mcp:latest - container_name: yargi-mcp-server - ports: - - "${PORT:-8000}:8000" - environment: - - HOST=0.0.0.0 - - PORT=8000 - - LOG_LEVEL=${LOG_LEVEL:-info} - - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-*} - - API_TOKEN=${API_TOKEN:-} - - PYTHONUNBUFFERED=1 - volumes: - # Mount logs directory - - ./logs:/app/logs - # Mount .env file if it exists - - ./.env:/app/.env:ro - restart: unless-stopped - healthcheck: - test: ["CMD", "python", "-c", "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - networks: - - yargi-network - - # Optional: Nginx reverse proxy - nginx: - image: nginx:alpine - container_name: yargi-nginx - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro - - ./ssl:/etc/nginx/ssl:ro - depends_on: - - yargi-mcp - networks: - - yargi-network - profiles: - - production - - # Optional: Redis for caching (future enhancement) - redis: - image: redis:alpine - container_name: yargi-redis - command: redis-server --appendonly yes - volumes: - - redis-data:/data - networks: - - yargi-network - profiles: - - with-cache - -networks: - yargi-network: - driver: bridge - -volumes: - redis-data: \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/docs/DEPLOYMENT.md b/saidsurucu-yargi-mcp-f5fa007/docs/DEPLOYMENT.md deleted file mode 100644 index db5f361..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/docs/DEPLOYMENT.md +++ /dev/null @@ -1,428 +0,0 @@ -# Yargı MCP Server Dağıtım Rehberi - -Bu rehber, Yargı MCP Server'ın ASGI web servisi olarak çeşitli dağıtım seçeneklerini kapsar. - -## İçindekiler - -- [Hızlı Başlangıç](#hızlı-başlangıç) -- [Yerel Geliştirme](#yerel-geliştirme) -- [Production Dağıtımı](#production-dağıtımı) -- [Cloud Dağıtımı](#cloud-dağıtımı) -- [Docker Dağıtımı](#docker-dağıtımı) -- [Güvenlik Hususları](#güvenlik-hususları) -- [İzleme](#izleme) - -## Hızlı Başlangıç - -### 1. Bağımlılıkları Yükleyin - -```bash -# ASGI sunucusu için uvicorn yükleyin -pip install uvicorn - -# Veya tüm bağımlılıklarla birlikte yükleyin -pip install -e . -pip install uvicorn -``` - -### 2. Sunucuyu Çalıştırın - -```bash -# Temel başlatma -python run_asgi.py - -# Veya doğrudan uvicorn ile -uvicorn asgi_app:app --host 0.0.0.0 --port 8000 -``` - -Sunucu şu adreslerde kullanılabilir olacak: -- MCP Endpoint: `http://localhost:8000/mcp/` -- Sağlık Kontrolü: `http://localhost:8000/health` -- API Durumu: `http://localhost:8000/status` - -## Yerel Geliştirme - -### Otomatik Yeniden Yükleme ile Geliştirme Sunucusu - -```bash -python run_asgi.py --reload --log-level debug -``` - -### FastAPI Entegrasyonunu Kullanma - -Ek REST API endpoint'leri için: - -```bash -uvicorn fastapi_app:app --reload -``` - -Bu şunları sağlar: -- `/docs` adresinde interaktif API dokümantasyonu -- `/api/tools` adresinde araç listesi -- `/api/databases` adresinde veritabanı bilgileri - -### Ortam Değişkenleri - -`.env.example` dosyasını temel alarak bir `.env` dosyası oluşturun: - -```bash -cp .env.example .env -``` - -Temel değişkenler: -- `HOST`: Sunucu host adresi (varsayılan: 127.0.0.1) -- `PORT`: Sunucu portu (varsayılan: 8000) -- `ALLOWED_ORIGINS`: CORS kökenleri (virgülle ayrılmış) -- `LOG_LEVEL`: Log seviyesi (debug, info, warning, error) - -## Production Dağıtımı - -### 1. Uvicorn ile Çoklu Worker Kullanımı - -```bash -python run_asgi.py --host 0.0.0.0 --port 8000 --workers 4 -``` - -### 2. Gunicorn Kullanımı - -```bash -pip install gunicorn -gunicorn asgi_app:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 -``` - -### 3. Nginx Reverse Proxy ile - -1. Nginx'i yükleyin -2. Sağlanan `nginx.conf` dosyasını kullanın: - -```bash -sudo cp nginx.conf /etc/nginx/sites-available/yargi-mcp -sudo ln -s /etc/nginx/sites-available/yargi-mcp /etc/nginx/sites-enabled/ -sudo nginx -t -sudo systemctl reload nginx -``` - -### 4. Systemd Servisi - -`/etc/systemd/system/yargi-mcp.service` dosyasını oluşturun: - -```ini -[Unit] -Description=Yargı MCP Server -After=network.target - -[Service] -Type=exec -User=www-data -WorkingDirectory=/opt/yargi-mcp -Environment="PATH=/opt/yargi-mcp/venv/bin" -ExecStart=/opt/yargi-mcp/venv/bin/uvicorn asgi_app:app --host 0.0.0.0 --port 8000 --workers 4 -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target -``` - -Etkinleştirin ve başlatın: - -```bash -sudo systemctl enable yargi-mcp -sudo systemctl start yargi-mcp -``` - -## Cloud Dağıtımı - -### Heroku - -1. `Procfile` oluşturun: -``` -web: uvicorn asgi_app:app --host 0.0.0.0 --port $PORT -``` - -2. Dağıtın: -```bash -heroku create uygulama-isminiz -git push heroku main -``` - -### Railway - -1. `railway.json` ekleyin: -```json -{ - "build": { - "builder": "NIXPACKS" - }, - "deploy": { - "startCommand": "uvicorn asgi_app:app --host 0.0.0.0 --port $PORT" - } -} -``` - -2. Railway CLI veya GitHub entegrasyonu ile dağıtın - -### Google Cloud Run - -1. Container oluşturun: -```bash -docker build -t yargi-mcp . -docker tag yargi-mcp gcr.io/PROJE_ADINIZ/yargi-mcp -docker push gcr.io/PROJE_ADINIZ/yargi-mcp -``` - -2. Dağıtın: -```bash -gcloud run deploy yargi-mcp \ - --image gcr.io/PROJE_ADINIZ/yargi-mcp \ - --platform managed \ - --region us-central1 \ - --allow-unauthenticated -``` - -### AWS Lambda (Mangum kullanarak) - -1. Mangum'u yükleyin: -```bash -pip install mangum -``` - -2. `lambda_handler.py` oluşturun: -```python -from mangum import Mangum -from asgi_app import app - -handler = Mangum(app, lifespan="off") -``` - -3. AWS SAM veya Serverless Framework kullanarak dağıtın - -## Docker Dağıtımı - -### Tek Container - -```bash -# Oluşturun -docker build -t yargi-mcp . - -# Çalıştırın -docker run -p 8000:8000 --env-file .env yargi-mcp -``` - -### Docker Compose - -```bash -# Geliştirme -docker-compose up - -# Nginx ile Production -docker-compose --profile production up - -# Redis önbellekleme ile -docker-compose --profile with-cache up -``` - -### Kubernetes - -Deployment YAML oluşturun: - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: yargi-mcp -spec: - replicas: 3 - selector: - matchLabels: - app: yargi-mcp - template: - metadata: - labels: - app: yargi-mcp - spec: - containers: - - name: yargi-mcp - image: yargi-mcp:latest - ports: - - containerPort: 8000 - env: - - name: HOST - value: "0.0.0.0" - - name: PORT - value: "8000" - livenessProbe: - httpGet: - path: /health - port: 8000 - initialDelaySeconds: 10 - periodSeconds: 30 ---- -apiVersion: v1 -kind: Service -metadata: - name: yargi-mcp-service -spec: - selector: - app: yargi-mcp - ports: - - port: 80 - targetPort: 8000 - type: LoadBalancer -``` - -## Güvenlik Hususları - -### 1. Kimlik Doğrulama - -`API_TOKEN` ortam değişkenini ayarlayarak token kimlik doğrulamasını etkinleştirin: - -```bash -export API_TOKEN=gizli-token-degeri -``` - -Ardından isteklere ekleyin: -```bash -curl -H "Authorization: Bearer gizli-token-degeri" http://localhost:8000/api/tools -``` - -### 2. HTTPS/SSL - -Production için her zaman HTTPS kullanın: - -1. SSL sertifikası edinin (Let's Encrypt vb.) -2. Nginx veya cloud sağlayıcıda yapılandırın -3. `ALLOWED_ORIGINS` değerini https:// kullanacak şekilde güncelleyin - -### 3. Rate Limiting (Hız Sınırlama) - -Sağlanan Nginx yapılandırması rate limiting içerir: -- API endpoint'leri: 10 istek/saniye -- MCP endpoint: 100 istek/saniye - -### 4. CORS Yapılandırması - -Production için belirli kaynaklara izin verin: - -```bash -ALLOWED_ORIGINS=https://app.sizindomain.com,https://www.sizindomain.com -``` - -## İzleme - -### Sağlık Kontrolleri - -`/health` endpoint'ini izleyin: - -```bash -curl http://localhost:8000/health -``` - -Yanıt: -```json -{ - "status": "healthy", - "timestamp": "2024-12-26T10:00:00", - "uptime_seconds": 3600, - "tools_operational": true -} -``` - -### Loglama - -Ortam değişkeni ile log seviyesini yapılandırın: - -```bash -LOG_LEVEL=info # veya debug, warning, error -``` - -Loglar şuraya yazılır: -- Konsol (stdout) -- `logs/mcp_server.log` dosyası - -### Metrikler (Opsiyonel) - -OpenTelemetry desteği için: - -```bash -pip install opentelemetry-instrumentation-fastapi -``` - -Ortam değişkenlerini ayarlayın: -```bash -OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 -OTEL_SERVICE_NAME=yargi-mcp-server -``` - -## Sorun Giderme - -### Port Zaten Kullanımda - -```bash -# 8000 portunu kullanan işlemi bulun -lsof -i :8000 - -# İşlemi sonlandırın -kill -9 -``` - -### İzin Hataları - -Dosya izinlerinin doğru olduğundan emin olun: - -```bash -chmod +x run_asgi.py -chown -R www-data:www-data /opt/yargi-mcp -``` - -### Bellek Sorunları - -Büyük belge işleme için worker belleğini artırın: - -```bash -# systemd servisinde -Environment="PYTHONMALLOC=malloc" -LimitNOFILE=65536 -``` - -### Zaman Aşımı Sorunları - -Zaman aşımlarını ayarlayın: -1. Uvicorn: `--timeout-keep-alive 75` -2. Nginx: `proxy_read_timeout 300s;` -3. Cloud sağlayıcılar: Platform özel zaman aşımı ayarlarını kontrol edin - -## Performans Ayarlama - -### 1. Worker İşlemleri - -- Geliştirme: 1 worker -- Production: CPU çekirdeği başına 2-4 worker - -### 2. Bağlantı Havuzlama - -Sunucu varsayılan olarak httpx ile bağlantı havuzlama kullanır. - -### 3. Önbellekleme (Gelecek Geliştirme) - -Redis önbellekleme docker-compose ile etkinleştirilebilir: - -```bash -docker-compose --profile with-cache up -``` - -### 4. Veritabanı Zaman Aşımları - -`.env` dosyasında veritabanı başına zaman aşımlarını ayarlayın: - -```bash -YARGITAY_TIMEOUT=60 -DANISTAY_TIMEOUT=60 -ANAYASA_TIMEOUT=90 -``` - -## Destek - -Sorunlar ve sorular için: -- GitHub Issues: https://github.com/saidsurucu/yargi-mcp/issues -- Dokümantasyon: README.md dosyasına bakın \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/client.py deleted file mode 100644 index 235d515..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/client.py +++ /dev/null @@ -1,177 +0,0 @@ -# emsal_mcp_module/client.py - -import httpx -# from bs4 import BeautifulSoup # Uncomment if needed for advanced HTML pre-processing -from typing import Dict, Any, List, Optional -import logging -import html -import re -import io -from markitdown import MarkItDown - -from .models import ( - EmsalSearchRequest, - EmsalDetailedSearchRequestData, - EmsalApiResponse, - EmsalDocumentMarkdown -) - -logger = logging.getLogger(__name__) -if not logger.hasHandlers(): - logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - -class EmsalApiClient: - """API Client for Emsal (UYAP Precedent Decision) search system.""" - BASE_URL = "https://emsal.uyap.gov.tr" - DETAILED_SEARCH_ENDPOINT = "/aramadetaylist" - DOCUMENT_ENDPOINT = "/getDokuman" - - def __init__(self, request_timeout: float = 30.0): - self.http_client = httpx.AsyncClient( - base_url=self.BASE_URL, - headers={ - "Content-Type": "application/json; charset=UTF-8", - "Accept": "application/json, text/plain, */*", - "X-Requested-With": "XMLHttpRequest", - }, - timeout=request_timeout, - verify=False # As per user's original FastAPI code - ) - - async def search_detailed_decisions( - self, - params: EmsalSearchRequest - ) -> EmsalApiResponse: - """Performs a detailed search for Emsal decisions.""" - - data_for_api_payload = EmsalDetailedSearchRequestData( - arananKelime=params.keyword or "", - Bam_Hukuk_Mahkemeleri=params.selected_bam_civil_court, # Uses alias "Bam Hukuk Mahkemeleri" - Hukuk_Mahkemeleri=params.selected_civil_court, # Uses alias "Hukuk Mahkemeleri" - birimHukukMah="+".join(params.selected_regional_civil_chambers) if params.selected_regional_civil_chambers else "", - esasYil=params.case_year_esas or "", - esasIlkSiraNo=params.case_start_seq_esas or "", - esasSonSiraNo=params.case_end_seq_esas or "", - kararYil=params.decision_year_karar or "", - kararIlkSiraNo=params.decision_start_seq_karar or "", - kararSonSiraNo=params.decision_end_seq_karar or "", - baslangicTarihi=params.start_date or "", - bitisTarihi=params.end_date or "", - siralama=params.sort_criteria, - siralamaDirection=params.sort_direction, - pageSize=params.page_size, - pageNumber=params.page_number - ) - - # Create request dict and remove empty string fields to avoid API issues - payload_dict = data_for_api_payload.model_dump(by_alias=True, exclude_none=True) - # Remove empty string fields that might cause API issues - cleaned_payload = {k: v for k, v in payload_dict.items() if v != ""} - final_payload = {"data": cleaned_payload} - - logger.info(f"EmsalApiClient: Performing DETAILED search with payload: {final_payload}") - return await self._execute_api_search(self.DETAILED_SEARCH_ENDPOINT, final_payload) - - async def _execute_api_search(self, endpoint: str, payload: Dict) -> EmsalApiResponse: - """Helper method to execute search POST request and process response for Emsal.""" - try: - response = await self.http_client.post(endpoint, json=payload) - response.raise_for_status() - response_json_data = response.json() - logger.debug(f"EmsalApiClient: Raw API response from {endpoint}: {response_json_data}") - - api_response_parsed = EmsalApiResponse(**response_json_data) - - if api_response_parsed.data and api_response_parsed.data.data: - for decision_item in api_response_parsed.data.data: - if decision_item.id: - decision_item.document_url = f"{self.BASE_URL}{self.DOCUMENT_ENDPOINT}?id={decision_item.id}" - - return api_response_parsed - except httpx.RequestError as e: - logger.error(f"EmsalApiClient: HTTP request error during Emsal search to {endpoint}: {e}") - raise - except Exception as e: - logger.error(f"EmsalApiClient: Error processing or validating Emsal search response from {endpoint}: {e}") - raise - - def _clean_html_and_convert_to_markdown_emsal(self, html_content_from_api_data_field: str) -> Optional[str]: - """ - Cleans HTML (from Emsal API 'data' field containing HTML string) - and converts it to Markdown using MarkItDown. - This assumes Emsal /getDokuman response is JSON with HTML in "data" field, - similar to Yargitay and the last Emsal /getDokuman example. - """ - if not html_content_from_api_data_field: - return None - - # Basic HTML unescaping and fixing common escaped characters - # Based on user's original fix_html_content in app/routers/emsal.py - content = html.unescape(html_content_from_api_data_field) - content = content.replace('\\"', '"') - content = content.replace('\\r\\n', '\n') - content = content.replace('\\n', '\n') - content = content.replace('\\t', '\t') - - # The HTML string from "data" field starts with "..." - html_input_for_markdown = content - - markdown_text = None - try: - # Convert HTML string to bytes and create BytesIO stream - html_bytes = html_input_for_markdown.encode('utf-8') - html_stream = io.BytesIO(html_bytes) - - # Pass BytesIO stream to MarkItDown to avoid temp file creation - md_converter = MarkItDown() - conversion_result = md_converter.convert(html_stream) - markdown_text = conversion_result.text_content - logger.info("EmsalApiClient: HTML to Markdown conversion successful.") - except Exception as e: - logger.error(f"EmsalApiClient: Error during MarkItDown HTML to Markdown conversion for Emsal: {e}") - - return markdown_text - - async def get_decision_document_as_markdown(self, id: str) -> EmsalDocumentMarkdown: - """ - Retrieves a specific Emsal decision by ID and returns its content as Markdown. - Assumes Emsal /getDokuman endpoint returns JSON with HTML content in the 'data' field. - """ - document_api_url = f"{self.DOCUMENT_ENDPOINT}?id={id}" - source_url = f"{self.BASE_URL}{document_api_url}" - logger.info(f"EmsalApiClient: Fetching Emsal document for Markdown (ID: {id}) from {source_url}") - - try: - response = await self.http_client.get(document_api_url) - response.raise_for_status() - - # Emsal /getDokuman returns JSON with HTML in 'data' field (confirmed by user example) - response_json = response.json() - html_content_from_api = response_json.get("data") - - if not isinstance(html_content_from_api, str) or not html_content_from_api.strip(): - logger.warning(f"EmsalApiClient: Received empty or non-string HTML in 'data' field for Emsal ID {id}.") - return EmsalDocumentMarkdown(id=id, markdown_content=None, source_url=source_url) - - markdown_content = self._clean_html_and_convert_to_markdown_emsal(html_content_from_api) - - return EmsalDocumentMarkdown( - id=id, - markdown_content=markdown_content, - source_url=source_url - ) - except httpx.RequestError as e: - logger.error(f"EmsalApiClient: HTTP error fetching Emsal document (ID: {id}): {e}") - raise - except ValueError as e: - logger.error(f"EmsalApiClient: ValueError processing Emsal document response (ID: {id}): {e}") - raise - except Exception as e: - logger.error(f"EmsalApiClient: General error processing Emsal document (ID: {id}): {e}") - raise - - async def close_client_session(self): - """Closes the HTTPX client session.""" - if self.http_client and not self.http_client.is_closed: - await self.http_client.aclose() - logger.info("EmsalApiClient: HTTP client session closed.") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/models.py deleted file mode 100644 index c94f0dd..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/emsal_mcp_module/models.py +++ /dev/null @@ -1,101 +0,0 @@ -# emsal_mcp_module/models.py - -from pydantic import BaseModel, Field, HttpUrl, ConfigDict -from typing import List, Optional, Dict, Any - -class EmsalDetailedSearchRequestData(BaseModel): - """ - Internal model for the 'data' object in the Emsal detailed search payload. - Field names use aliases to match the exact keys in the API payload - (e.g., "Bam Hukuk Mahkemeleri" with spaces). - The API expects empty strings for None/omitted optional fields. - """ - arananKelime: Optional[str] = "" - - Bam_Hukuk_Mahkemeleri: str = Field("", alias="Bam Hukuk Mahkemeleri") - Hukuk_Mahkemeleri: str = Field("", alias="Hukuk Mahkemeleri") - # Add other specific court type fields from the form if they are separate keys in payload - # E.g., "Ceza Mahkemeleri", "İdari Mahkemeler" etc. - - birimHukukMah: Optional[str] = Field("", description="Regional chambers (+ separated)") - - esasYil: Optional[str] = "" - esasIlkSiraNo: Optional[str] = "" - esasSonSiraNo: Optional[str] = "" - kararYil: Optional[str] = "" - kararIlkSiraNo: Optional[str] = "" - kararSonSiraNo: Optional[str] = "" - baslangicTarihi: Optional[str] = "" - bitisTarihi: Optional[str] = "" - siralama: str # Mandatory in payload example - siralamaDirection: str # Mandatory in payload example - pageSize: int - pageNumber: int - - model_config = ConfigDict(populate_by_name=True) # Enables use of alias in serialization (when dumping to dict for payload) - -class EmsalSearchRequest(BaseModel): # This is the model the MCP tool will accept - """Model for Emsal detailed search request, with user-friendly field names.""" - keyword: str = Field("", description="Keyword") - - selected_bam_civil_court: str = Field("", description="BAM Civil Court") - selected_civil_court: str = Field("", description="Civil Court") - selected_regional_civil_chambers: List[str] = Field(default_factory=list, description="Regional chambers") - - case_year_esas: str = Field("", description="Case year") - case_start_seq_esas: str = Field("", description="Start case no") - case_end_seq_esas: str = Field("", description="End case no") - - decision_year_karar: str = Field("", description="Decision year") - decision_start_seq_karar: str = Field("", description="Start decision no") - decision_end_seq_karar: str = Field("", description="End decision no") - - start_date: str = Field("", description="Start date (DD.MM.YYYY)") - end_date: str = Field("", description="End date (DD.MM.YYYY)") - - sort_criteria: str = Field("1", description="Sort by") - sort_direction: str = Field("desc", description="Direction") - - page_number: int = Field(default=1, ge=1) - page_size: int = Field(default=10, ge=1, le=10) - - -class EmsalApiDecisionEntry(BaseModel): - """Model for an individual decision entry from the Emsal API search response.""" - id: str - daire: str = Field("", description="Chamber") - esasNo: str = Field("", description="Case number") - kararNo: str = Field("", description="Decision number") - kararTarihi: str = Field("", description="Decision date") - arananKelime: str = Field("", description="Keyword") - durum: str = Field("", description="Status") - # index: Optional[int] = None # Present in Emsal response, can be added if tool needs it - - document_url: Optional[HttpUrl] = Field(None, description="Document URL") - - model_config = ConfigDict(extra='ignore') - -class EmsalApiResponseInnerData(BaseModel): - """Model for the inner 'data' object in the Emsal API search response.""" - data: List[EmsalApiDecisionEntry] - recordsTotal: int - recordsFiltered: int - draw: int = Field(0, description="Draw counter (Çizim Sayıcısı) from API, usually for DataTables.") - -class EmsalApiResponse(BaseModel): - """Model for the complete search response from the Emsal API.""" - data: EmsalApiResponseInnerData - metadata: Optional[Dict[str, Any]] = Field(None, description="Optional metadata (Meta Veri) from API, if any.") - -class EmsalDocumentMarkdown(BaseModel): - """Model for an Emsal decision document, containing only Markdown content.""" - id: str - markdown_content: str = Field("", description="The decision content (Karar İçeriği) converted to Markdown.") - source_url: HttpUrl - -class CompactEmsalSearchResult(BaseModel): - """A compact search result model for the MCP tool to return.""" - decisions: List[EmsalApiDecisionEntry] - total_records: int - requested_page: int - page_size: int \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/example_fastapi_app.py b/saidsurucu-yargi-mcp-f5fa007/example_fastapi_app.py deleted file mode 100644 index 5e4c3fd..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/example_fastapi_app.py +++ /dev/null @@ -1,1680 +0,0 @@ -""" -FastAPI Comprehensive Endpoints with Complete MCP Documentation -This is the complete version with all descriptions and docstrings from MCP server. -""" - -import os -from typing import List, Dict, Any, Optional -from datetime import datetime - -from fastapi import FastAPI, HTTPException, Query, Depends, Body -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse -from pydantic import BaseModel, Field -import json - -# Import the main MCP app -from mcp_server_main import app as mcp_server - -# Create MCP ASGI app -mcp_asgi_app = mcp_server.http_app(path="/mcp") - -# Create FastAPI app with MCP lifespan -app = FastAPI( - title="Yargı MCP API - Turkish Legal Database REST API", - description=""" - Comprehensive REST API for Turkish Legal Databases with complete MCP tool coverage. - - This API provides access to 8 major Turkish legal institutions including: - • Yargıtay (Court of Cassation) - Supreme civil/criminal court - • Danıştay (Council of State) - Supreme administrative court - • Constitutional Court - Constitutional review and individual applications - • Competition Authority - Antitrust and merger decisions - • Public Procurement Authority - Government contracting disputes - • Court of Accounts - Public audit and accountability - • Emsal (UYAP Precedents) - Cross-court precedent database - • Local and Appellate Courts - First and second instance decisions - - Features complete coverage of 33 MCP tools with enhanced documentation, - typed request models, and comprehensive legal context. - - Tool Properties (from MCP annotations): - • Read-only: All tools are read-only and do not modify system state - • Idempotent: Same inputs produce same outputs for reliable research - • Open-world search: Search tools explore comprehensive legal databases - • Deterministic document retrieval: Document tools return consistent content - """, - version="1.0.0", - lifespan=mcp_asgi_app.lifespan -) - -# Add CORS middleware -cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",") -app.add_middleware( - CORSMiddleware, - allow_origins=cors_origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Mount MCP server -app.mount("/mcp-server", mcp_asgi_app) - -# Response models (keeping from original) -class ToolInfo(BaseModel): - name: str - description: str - parameters: Dict[str, Any] - -class ServerInfo(BaseModel): - name: str - version: str - description: str - tools_count: int - databases: List[str] - mcp_endpoint: str - api_docs: str - -class HealthCheck(BaseModel): - status: str - timestamp: datetime - uptime_seconds: Optional[float] = None - tools_operational: bool - -# Track server start time -SERVER_START_TIME = datetime.now() - -# MCP tool caller helper -async def call_mcp_tool(tool_name: str, arguments: Dict[str, Any]): - """Call an MCP tool with given arguments""" - try: - tool = mcp_server._tool_manager._tools.get(tool_name) - if not tool: - raise HTTPException(status_code=404, detail=f"Tool '{tool_name}' not found") - result = await tool.fn(**arguments) - return result - except Exception as e: - raise HTTPException(status_code=500, detail=f"Tool execution failed: {str(e)}") - -# ============================================================================ -# COMPREHENSIVE REQUEST MODELS WITH FULL MCP DOCUMENTATION -# ============================================================================ - -class YargitaySearchRequest(BaseModel): - """ - Search request for Court of Cassation (Yargıtay) decisions using primary official API. - - The Court of Cassation is Turkey's highest court for civil and criminal matters, - equivalent to a Supreme Court. Provides access to comprehensive supreme court precedents. - """ - arananKelime: str = Field( - ..., - description="""Keyword to search for with advanced operators: - • Space between words = OR logic (arsa payı → "arsa" OR "payı") - • "exact phrase" = Exact match ("arsa payı" → exact phrase) - • word1+word2 = AND logic (arsa+payı → both words required) - • word* = Wildcard (bozma* → bozma, bozması, bozmanın, etc.) - • +"phrase1" +"phrase2" = Multiple required phrases - • +"required" -"excluded" = Include and exclude - - Turkish Examples: - • Simple OR: arsa payı (~523K results) - • Exact phrase: "arsa payı" (~22K results) - • Multiple AND: +"arsa payı" +"bozma sebebi" (~234 results) - • Wildcard: bozma* (bozma, bozması, bozmanın, etc.) - • Exclude: +"arsa payı" -"kira sözleşmesi" - """, - example='+"mülkiyet hakkı" +"iptal"' - ) - birimYrgKurulDaire: Optional[str] = Field( - "", - description="""Chamber/board selection (52 options): - Civil Chambers: 1-23. Hukuk Dairesi - Criminal Chambers: 1-23. Ceza Dairesi - General Assemblies: Hukuk Genel Kurulu, Ceza Genel Kurulu - Special Boards: Hukuk/Ceza Daireleri Başkanlar Kurulu, Büyük Genel Kurulu - - Use "" for ALL chambers or specify exact chamber name. - """, - example="1. Hukuk Dairesi" - ) - baslangicTarihi: Optional[str] = Field(None, description="Start date (DD.MM.YYYY)", example="01.01.2020") - bitisTarihi: Optional[str] = Field(None, description="End date (DD.MM.YYYY)", example="31.12.2024") - pageSize: int = Field(20, description="Results per page (1-100)", ge=1, le=100, example=20) - -class YargitayBedestenSearchRequest(BaseModel): - """ - Search request for Court of Cassation using Bedesten API (alternative source). - Complements primary API with different search capabilities and recent decisions. - """ - phrase: str = Field( - ..., - description="""Aranacak kavram/kelime. İki farklı arama türü desteklenir: - • Normal arama: "mülkiyet hakkı" - kelimeler ayrı ayrı aranır - • Tam cümle arama: "\"mülkiyet hakkı\"" - tırnak içindeki ifade aynen aranır - Tam cümle aramalar daha kesin sonuçlar verir. - - Search phrase with exact matching support: - • Regular search: "mülkiyet hakkı" - searches individual words separately - • Exact phrase search: "\"mülkiyet hakkı\"" - searches for exact phrase as unit - Exact phrase search provides more precise results with fewer false positives. - """, - example="\"mülkiyet hakkı\"" - ) - birimAdi: Optional[str] = Field( - None, - description="""Daire/Kurul seçimi (52 seçenek - ana API ile aynı): - • Hukuk daireleri: 1. Hukuk Dairesi - 23. Hukuk Dairesi - • Ceza daireleri: 1. Ceza Dairesi - 23. Ceza Dairesi - • Genel kurullar: Hukuk Genel Kurulu, Ceza Genel Kurulu - • Özel kurullar: Hukuk/Ceza Daireleri Başkanlar Kurulu, Büyük Genel Kurulu - - Chamber filtering (52 options - same as primary API): - • Civil chambers: 1. Hukuk Dairesi through 23. Hukuk Dairesi - • Criminal chambers: 1. Ceza Dairesi through 23. Ceza Dairesi - • General assemblies: Hukuk Genel Kurulu, Ceza Genel Kurulu - • Special boards: Hukuk/Ceza Daireleri Başkanlar Kurulu, Büyük Genel Kurulu - - Use None for ALL chambers, or specify exact chamber name. - """, - example="1. Hukuk Dairesi" - ) - kararTarihiStart: Optional[str] = Field( - None, - description="""Karar başlangıç tarihi (ISO 8601 formatı): - Format: YYYY-MM-DDTHH:MM:SS.000Z - Örnek: "2024-01-01T00:00:00.000Z" - 1 Ocak 2024'ten itibaren kararlar - kararTarihiEnd ile birlikte tarih aralığı filtrelemesi için kullanılır. - - Decision start date filter (ISO 8601 format): - Format: YYYY-MM-DDTHH:MM:SS.000Z - Example: "2024-01-01T00:00:00.000Z" for decisions from Jan 1, 2024 - Use with kararTarihiEnd for date range filtering. - """, - example="2024-01-01T00:00:00.000Z" - ) - kararTarihiEnd: Optional[str] = Field( - None, - description="""Karar bitiş tarihi (ISO 8601 formatı): - Format: YYYY-MM-DDTHH:MM:SS.000Z - Örnek: "2024-12-31T23:59:59.999Z" - 31 Aralık 2024'e kadar kararlar - kararTarihiStart ile birlikte tarih aralığı filtrelemesi için kullanılır. - - Decision end date filter (ISO 8601 format): - Format: YYYY-MM-DDTHH:MM:SS.000Z - Example: "2024-12-31T23:59:59.999Z" for decisions until Dec 31, 2024 - Use with kararTarihiStart for date range filtering. - """, - example="2024-12-31T23:59:59.999Z" - ) - pageSize: int = Field(20, description="Results per page (1-100)", ge=1, le=100) - -class DanistayKeywordSearchRequest(BaseModel): - """ - Keyword-based search for Council of State (Danıştay) decisions with Boolean logic. - - The Council of State is Turkey's highest administrative court, providing final - rulings on administrative law matters with Boolean keyword operators. - """ - andKelimeler: List[str] = Field( - ..., - description="ALL keywords must be present (AND logic)", - example=["idari işlem", "iptal"] - ) - orKelimeler: Optional[List[str]] = Field( - None, - description="ANY keyword can be present (OR logic)", - example=["ruhsat", "izin", "lisans"] - ) - notKelimeler: Optional[List[str]] = Field( - None, - description="EXCLUDE if keywords present (NOT logic)", - example=["vergi"] - ) - pageSize: int = Field(20, description="Results per page (1-100)", ge=1, le=100) - -class DanistayDetailedSearchRequest(BaseModel): - """ - Detailed search for Council of State decisions with comprehensive filtering. - Provides the most comprehensive search capabilities for administrative court decisions. - """ - daire: Optional[str] = Field( - None, - description="Chamber/Department filter (1. Daire through 17. Daire, special councils)", - example="3. Daire" - ) - baslangicTarihi: Optional[str] = Field(None, description="Start date (DD.MM.YYYY)", example="01.01.2020") - bitisTarihi: Optional[str] = Field(None, description="End date (DD.MM.YYYY)", example="31.12.2024") - esas: Optional[str] = Field(None, description="Case number (Esas No)", example="2024/123") - karar: Optional[str] = Field(None, description="Decision number (Karar No)", example="2024/456") - -class DanistayBedestenSearchRequest(BaseModel): - """ - Council of State search using Bedesten API with chamber filtering and exact phrase search. - Provides access to administrative court decisions with 27 chamber options. - """ - phrase: str = Field(..., description="Search phrase (supports exact matching with quotes)") - birimAdi: Optional[str] = Field( - None, - description="""Chamber filtering (27 options): - Main Councils: Büyük Gen.Kur., İdare Dava Daireleri Kurulu, Vergi Dava Daireleri Kurulu - Chambers: 1. Daire through 17. Daire - Military: Askeri Yüksek İdare Mahkemesi chambers - """, - example="3. Daire" - ) - kararTarihiStart: Optional[str] = Field(None, description="Start date (ISO 8601)") - kararTarihiEnd: Optional[str] = Field(None, description="End date (ISO 8601)") - pageSize: int = Field(20, description="Results per page", ge=1, le=100) - -class EmsalSearchRequest(BaseModel): - """ - Search Precedent (Emsal) decisions from UYAP system across multiple court levels. - Provides access to precedent decisions from various Turkish courts. - """ - keyword: str = Field(..., description="Search keyword across decision texts") - decision_year_karar: Optional[str] = Field(None, description="Decision year filter", example="2024") - results_per_page: int = Field(20, description="Results per page", ge=1, le=100) - -class UyusmazlikSearchRequest(BaseModel): - """ - Search Court of Jurisdictional Disputes decisions. - Resolves jurisdictional disputes between different court systems. - """ - keywords: List[str] = Field(..., description="Search keywords", example=["görev", "uyuşmazlık"]) - page_to_fetch: int = Field(1, description="Page number", ge=1) - -class AnayasaNormSearchRequest(BaseModel): - """ - Search Constitutional Court norm control (judicial review) decisions. - Turkey's highest constitutional authority for reviewing law constitutionality. - """ - keywords_all: List[str] = Field(..., description="All required keywords", example=["eğitim hakkı", "anayasa"]) - period: Optional[str] = Field(None, description="Constitutional period (1=1961, 2=1982)", example="2") - application_type: Optional[str] = Field(None, description="Application type (1=İptal)", example="1") - results_per_page: int = Field(20, description="Results per page", ge=1, le=100) - -class AnayasaBireyselSearchRequest(BaseModel): - """ - Search Constitutional Court individual application decisions. - Human rights violation cases through individual citizen petitions. - """ - keywords: List[str] = Field(..., description="Search keywords", example=["ifade özgürlüğü", "basın"]) - page_to_fetch: int = Field(1, description="Page number", ge=1) - -class KikSearchRequest(BaseModel): - """ - Search Public Procurement Authority (KİK) decisions. - Government procurement disputes and regulatory interpretations. - """ - karar_tipi: Optional[str] = Field( - None, - description="Decision type (rbUyusmazlik=Disputes, rbDuzenleyici=Regulatory, rbMahkeme=Court)", - example="rbUyusmazlik" - ) - karar_metni: Optional[str] = Field(None, description="Decision text search", example="ihale iptali") - basvuru_konusu_ihale: Optional[str] = Field(None, description="Tender subject", example="danışmanlık") - karar_tarihi_baslangic: Optional[str] = Field(None, description="Start date", example="01.01.2023") - -class RekabetSearchRequest(BaseModel): - """ - Search Competition Authority decisions. - Antitrust, merger control, and competition law enforcement. - """ - KararTuru: Optional[str] = Field( - None, - description="Decision type (Birleşme ve Devralma, Rekabet İhlali, Muafiyet, etc.)", - example="Birleşme ve Devralma" - ) - PdfText: Optional[str] = Field( - None, - description="Full-text search in decisions. Use quotes for exact phrases.", - example="\"market definition\" telecommunications" - ) - YayinlanmaTarihi: Optional[str] = Field(None, description="Publication date", example="01.01.2020") - page: int = Field(1, description="Page number", ge=1) - -class BedestenSearchRequest(BaseModel): - """ - Generic search request for Bedesten API courts (Yerel Hukuk, İstinaf Hukuk, KYB). - Supports exact phrase search and date filtering. - """ - phrase: str = Field( - ..., - description="Search phrase. Use quotes for exact matching: \"legal term\"", - example="\"sözleşme ihlali\"" - ) - kararTarihiStart: Optional[str] = Field(None, description="Start date (ISO 8601)") - kararTarihiEnd: Optional[str] = Field(None, description="End date (ISO 8601)") - pageSize: int = Field(20, description="Results per page", ge=1, le=100) - -class SayistaySearchRequest(BaseModel): - """ - Search Court of Accounts (Sayıştay) decisions. - Public audit, accountability, and financial oversight decisions. - """ - keywords: List[str] = Field(..., description="Search keywords", example=["mali sorumluluk", "denetim"]) - page_to_fetch: int = Field(1, description="Page number", ge=1) - -# ============================================================================ -# BASIC SERVER ENDPOINTS (keeping from original) -# ============================================================================ - -@app.get("/", response_model=ServerInfo) -async def root(): - """Get comprehensive server information with database coverage""" - return ServerInfo( - name="Yargı MCP Server - Turkish Legal Database API", - version="1.0.0", - description="Complete REST API for Turkish legal databases with 33 MCP tools", - tools_count=len(mcp_server._tool_manager._tools), - databases=[ - "Yargıtay (Court of Cassation) - 4 tools", - "Danıştay (Council of State) - 5 tools", - "Emsal (UYAP Precedents) - 2 tools", - "Uyuşmazlık Mahkemesi (Jurisdictional Disputes) - 2 tools", - "Anayasa Mahkemesi (Constitutional Court) - 4 tools", - "Kamu İhale Kurulu (Public Procurement) - 2 tools", - "Rekabet Kurumu (Competition Authority) - 2 tools", - "Sayıştay (Court of Accounts) - 6 tools", - "Bedesten API Courts (Local/Appellate/KYB) - 6 tools" - ], - mcp_endpoint="/mcp-server/mcp/", - api_docs="/docs" - ) - -@app.get("/health", response_model=HealthCheck) -async def health_check(): - """Health check with comprehensive system status""" - uptime = (datetime.now() - SERVER_START_TIME).total_seconds() - return HealthCheck( - status="healthy", - timestamp=datetime.now(), - uptime_seconds=uptime, - tools_operational=len(mcp_server._tool_manager._tools) == 33 - ) - -@app.get("/api/tools", response_model=List[ToolInfo]) -async def list_tools( - search: Optional[str] = Query(None, description="Search tools by name or description"), - database: Optional[str] = Query(None, description="Filter by database name") -): - """List all 33 MCP tools with filtering capabilities""" - tools = [] - for tool in mcp_server._tool_manager._tools.values(): - if search and search.lower() not in tool.name.lower() and search.lower() not in tool.description.lower(): - continue - if database: - db_lower = database.lower() - if db_lower not in tool.name.lower() and db_lower not in tool.description.lower(): - continue - - params = {} - if hasattr(tool, 'schema') and tool.schema: - if hasattr(tool.schema, 'parameters'): - params = tool.schema.parameters - elif hasattr(tool.schema, '__annotations__'): - params = {k: str(v) for k, v in tool.schema.__annotations__.items()} - - tools.append(ToolInfo( - name=tool.name, - description=tool.description, - parameters=params - )) - return tools - -# ============================================================================ -# YARGITAY (COURT OF CASSATION) ENDPOINTS - 4 TOOLS -# ============================================================================ - -@app.post( - "/api/yargitay/search", - tags=["Yargıtay"], - summary="Search Court of Cassation (Primary API)", - description="""Search Turkey's Supreme Court for civil and criminal precedents using advanced operators. - -Key Features: -• Advanced search: AND (+), OR (space), NOT (-), wildcards (*), exact phrases ("") -• 52 chamber options (23 Civil + 23 Criminal + General Assemblies) -• Date range filtering • Case/decision number filtering • Pagination - -Search Examples: -• OR search: property share (finds ANY words) -• Exact phrase: "property share" (finds exact phrase) -• AND required: +"property share" +"annulment reason" -• Wildcard: construct* (construction, constructive, etc.) -• Exclude terms: +"property share" -"construction contract" - -Use for supreme court precedent research and legal principle analysis.""" -) -async def search_yargitay(request: YargitaySearchRequest): - """ - Searches Court of Cassation (Yargıtay) decisions using the primary official API. - - The Court of Cassation (Yargıtay) is Turkey's highest court for civil and criminal matters, - equivalent to a Supreme Court. This tool provides access to the most comprehensive database - of supreme court precedents with advanced search capabilities and filtering options. - - Key Features: - • Advanced search operators (AND, OR, wildcards, exclusions) - • Chamber filtering: 52 options (23 Civil (Hukuk) + 23 Criminal (Ceza) + General Assemblies (Genel Kurullar)) - • Date range filtering with DD.MM.YYYY format - • Case number filtering (Case No (Esas No) and Decision No (Karar No)) - • Pagination support (1-100 results per page) - • Multiple sorting options (by case number, decision number, date) - - SEARCH SYNTAX GUIDE: - • Words with spaces: OR search ("property share" finds ANY of the words) - • "Quotes": Exact phrase search ("property share" finds exact phrase) - • Plus sign (+): AND search (property+share requires both words) - • Asterisk (*): Wildcard (construct* matches variations) - • Minus sign (-): Exclude terms (avoid unwanted results) - - Common Search Patterns: - • Simple OR: property share (finds ~523K results) - • Exact phrase: "property share" (finds ~22K results) - • Multiple required: +"property share" +"annulment reason (bozma sebebi)" (finds ~234 results) - • Wildcard expansion: construct* (matches construction, constructive, etc.) - • Exclude unwanted: +"property share" -"construction contract" - - Use cases: - • Research supreme court precedents and legal principles - • Find decisions from specific chambers (Civil (Hukuk) vs Criminal (Ceza)) - • Search for interpretations of specific legal concepts - • Analyze court reasoning on complex legal issues - • Track legal developments over time periods - - Returns structured search results with decision metadata. Use get_yargitay_document_markdown() - to retrieve full decision texts for detailed analysis. - """ - args = { - "arananKelime": request.arananKelime, - "birimYrgKurulDaire": request.birimYrgKurulDaire, - "pageSize": request.pageSize - } - if request.baslangicTarihi: - args["baslangicTarihi"] = request.baslangicTarihi - if request.bitisTarihi: - args["bitisTarihi"] = request.bitisTarihi - return await call_mcp_tool("search_yargitay_detailed", args) - -@app.post( - "/api/yargitay/search-bedesten", - tags=["Yargıtay"], - summary="Search Court of Cassation (Bedesten API)", - description="""Alternative Court of Cassation search with exact phrase matching and recent decisions. - -Key Features: -• Exact phrase search: "\"legal term\"" for precise matching -• Regular search: "legal term" for individual word matching -• 52 chamber filtering options (same as primary API) -• ISO 8601 date filtering • Recent decision coverage - -Use alongside primary search for comprehensive coverage. Exact phrase search provides -higher precision with fewer false positives.""" -) -async def search_yargitay_bedesten(request: YargitayBedestenSearchRequest): - """Search Court of Cassation using Bedesten API. Complements primary API for complete coverage.""" - args = {"phrase": request.phrase, "pageSize": request.pageSize} - if request.birimAdi: - args["birimAdi"] = request.birimAdi - if request.kararTarihiStart: - args["kararTarihiStart"] = request.kararTarihiStart - if request.kararTarihiEnd: - args["kararTarihiEnd"] = request.kararTarihiEnd - return await call_mcp_tool("search_yargitay_bedesten", args) - -@app.get( - "/api/yargitay/document/{decision_id}", - tags=["Yargıtay"], - summary="Get Court of Cassation Document (Primary API)", - description="""Retrieve complete Court of Cassation decision in Markdown format. - -Content includes: -• Complete legal reasoning and precedent analysis -• Detailed examination of lower court decisions -• Citations of laws, regulations, and prior cases -• Final ruling with legal justification - -Perfect for detailed legal analysis, precedent research, and citation building.""" -) -async def get_yargitay_document(decision_id: str): - """Get full Court of Cassation decision text in clean Markdown format.""" - return await call_mcp_tool("get_yargitay_document_markdown", {"id": decision_id}) - -@app.get( - "/api/yargitay/bedesten-document/{document_id}", - tags=["Yargıtay"], - summary="Get Court of Cassation Document (Bedesten API)", - description="""Retrieve Court of Cassation decision from Bedesten API in Markdown format. - -Features: -• Supports both HTML and PDF source documents -• Clean Markdown conversion with legal structure preserved -• Removes technical artifacts for easy reading -• Compatible with documentId from Bedesten search results""" -) -async def get_yargitay_bedesten_document(document_id: str): - """Get Court of Cassation document from Bedesten API in Markdown format.""" - return await call_mcp_tool("get_yargitay_bedesten_document_markdown", {"documentId": document_id}) - -# ============================================================================ -# DANISTAY (COUNCIL OF STATE) ENDPOINTS - 5 TOOLS -# ============================================================================ - -@app.post( - "/api/danistay/search-keyword", - tags=["Danıştay"], - summary="Search Council of State (Keyword Logic)", - description="""Search Turkey's highest administrative court using Boolean keyword logic. - -Boolean Operators: -• AND keywords: ALL must be present (required terms) -• OR keywords: ANY can be present (alternative terms) -• NOT keywords: EXCLUDE if present (unwanted terms) - -Examples: -• Administrative acts: andKelimeler=["idari işlem", "iptal"] -• Permits/licenses: orKelimeler=["ruhsat", "izin", "lisans"] -• Exclude tax cases: notKelimeler=["vergi"] - -Perfect for administrative law research and government action reviews.""" -) -async def search_danistay_keyword(request: DanistayKeywordSearchRequest): - """ - Searches Council of State (Danıştay) decisions using keyword-based logic. - - The Council of State (Danıştay) is Turkey's highest administrative court, responsible for - reviewing administrative actions and providing administrative law precedents. This tool - provides flexible keyword-based searching with Boolean logic operators. - - Key Features: - • Boolean logic operators: AND, OR, NOT combinations - • Multiple keyword lists for complex search strategies - • Pagination support (1-100 results per page) - • Administrative law focus (permits, licenses, public administration) - • Complement to search_danistay_detailed for comprehensive coverage - - Keyword Logic: - • andKelimeler: ALL keywords must be present (AND logic) - • orKelimeler: ANY keyword can be present (OR logic) - • notAndKelimeler: EXCLUDE if ALL keywords present (NOT AND) - • notOrKelimeler: EXCLUDE if ANY keyword present (NOT OR) - - Administrative Law Use Cases: - • Research administrative court precedents - • Find decisions on specific government agencies - • Search for rulings on permits (ruhsat) and licenses (izin) - • Analyze administrative procedure interpretations - • Study public administration legal principles - - Examples: - • Simple AND: andKelimeler=["administrative act (idari işlem)", "annulment (iptal)"] - • OR search: orKelimeler=["permit (ruhsat)", "permission (izin)", "license (lisans)"] - • Complex: andKelimeler=["municipality (belediye)"], notOrKelimeler=["tax (vergi)"] - - Returns structured search results. Use get_danistay_document_markdown() for full texts. - For comprehensive Council of State (Danıştay) research, also use search_danistay_detailed and search_danistay_bedesten. - """ - args = {"andKelimeler": request.andKelimeler, "pageSize": request.pageSize} - if request.orKelimeler: - args["orKelimeler"] = request.orKelimeler - if request.notKelimeler: - args["notKelimeler"] = request.notKelimeler - return await call_mcp_tool("search_danistay_by_keyword", args) - -@app.post( - "/api/danistay/search-detailed", - tags=["Danıştay"], - summary="Search Council of State (Detailed Criteria)", - description="""Most comprehensive Council of State search with advanced filtering. - -Advanced Filtering: -• Chamber targeting (1. Daire through 17. Daire, special councils) -• Case/decision number ranges • Date range filtering -• Legislation cross-referencing • Multiple sorting options - -Use for specialized administrative law research, chamber-specific decisions, -and regulatory compliance analysis.""" -) -async def search_danistay_detailed(request: DanistayDetailedSearchRequest): - """Search Council of State with comprehensive filtering for specialized administrative law research.""" - args = {} - for field in ["daire", "baslangicTarihi", "bitisTarihi", "esas", "karar"]: - if getattr(request, field): - args[field] = getattr(request, field) - return await call_mcp_tool("search_danistay_detailed", args) - -@app.post( - "/api/danistay/search-bedesten", - tags=["Danıştay"], - summary="Search Council of State (Bedesten API)", - description="""Council of State search via Bedesten API with 27 chamber options and exact phrase search. - -Key Features: -• 27 chamber options (Main Councils, 17 Chambers, Military courts) -• Exact phrase search with double quotes for precision -• ISO 8601 date filtering • Alternative data source - -Use with other Danıştay tools for complete administrative law coverage.""" -) -async def search_danistay_bedesten(request: DanistayBedestenSearchRequest): - """Search Council of State via Bedesten API. Use with other Danıştay tools for complete coverage.""" - args = {"phrase": request.phrase, "pageSize": request.pageSize} - for field in ["birimAdi", "kararTarihiStart", "kararTarihiEnd"]: - if getattr(request, field): - args[field] = getattr(request, field) - return await call_mcp_tool("search_danistay_bedesten", args) - -@app.get( - "/api/danistay/document/{decision_id}", - tags=["Danıştay"], - summary="Get Council of State Document (Primary API)", - description="""Retrieve complete administrative court decision in Markdown format. - -Content includes: -• Complete administrative law reasoning and precedent analysis -• Review of administrative actions and government decisions -• Citations of administrative laws and regulations -• Final administrative ruling with legal justification - -Essential for administrative law research and government compliance analysis.""" -) -async def get_danistay_document(decision_id: str): - """Get full Council of State decision text in clean Markdown format.""" - return await call_mcp_tool("get_danistay_document_markdown", {"id": decision_id}) - -@app.get( - "/api/danistay/bedesten-document/{document_id}", - tags=["Danıştay"], - summary="Get Council of State Document (Bedesten API)", - description="""Retrieve Council of State decision from Bedesten API in Markdown format.""" -) -async def get_danistay_bedesten_document(document_id: str): - """Get Council of State document from Bedesten API in Markdown format.""" - return await call_mcp_tool("get_danistay_bedesten_document_markdown", {"documentId": document_id}) - -# ============================================================================ -# BEDESTEN API COURTS (LOCAL/APPELLATE/KYB) - 6 TOOLS -# ============================================================================ - -@app.post( - "/api/yerel-hukuk/search", - tags=["Yerel Hukuk"], - summary="Search Local Civil Courts", - description="""Search first-instance civil court decisions using Bedesten API. - -Local Civil Courts handle: -• Contract disputes • Property rights • Family law • Tort claims -• Commercial disputes • Consumer protection - -Only available tool for local court decisions. Supports exact phrase search -and date filtering for precise legal research.""" -) -async def search_yerel_hukuk(request: BedestenSearchRequest): - """ - Searches Yerel Hukuk Mahkemesi (Local Civil Court) decisions using Bedesten API. - - This provides access to local court decisions that are not available through other APIs. - Currently the only available tool for searching Yerel Hukuk Mahkemesi decisions. - Local civil courts represent the first instance of civil litigation in Turkey. - - Local Civil Courts handle: - • Contract disputes and commercial litigation - • Property rights and real estate disputes - • Family law matters (divorce, custody, inheritance) - • Tort claims and compensation cases - • Consumer protection issues - • Employment disputes - - Returns structured search results with decision metadata. Use get_yerel_hukuk_bedesten_document_markdown() - to retrieve full decision texts for detailed analysis. - """ - args = {"phrase": request.phrase, "pageSize": request.pageSize} - for field in ["kararTarihiStart", "kararTarihiEnd"]: - if getattr(request, field): - args[field] = getattr(request, field) - return await call_mcp_tool("search_yerel_hukuk_bedesten", args) - -@app.get("/api/yerel-hukuk/document/{document_id}", tags=["Yerel Hukuk"]) -async def get_yerel_hukuk_document(document_id: str): - """ - Retrieves a Yerel Hukuk Mahkemesi decision document from Bedesten API and converts to Markdown. - - This tool fetches complete local court decision texts using documentId from search results. - Perfect for detailed analysis of first-instance civil court rulings. - - Supports both HTML and PDF content types, automatically converting to clean Markdown format. - Use documentId from search_yerel_hukuk_bedesten results. - """ - return await call_mcp_tool("get_yerel_hukuk_bedesten_document_markdown", {"documentId": document_id}) - -@app.post( - "/api/istinaf-hukuk/search", - tags=["İstinaf Hukuk"], - summary="Search Civil Courts of Appeals", - description="""Search intermediate appellate court decisions using Bedesten API. - -İstinaf Courts are intermediate appellate courts handling appeals from local civil courts -before cases reach the Court of Cassation. Only available tool for İstinaf decisions.""" -) -async def search_istinaf_hukuk(request: BedestenSearchRequest): - """ - Searches İstinaf Hukuk Mahkemesi (Civil Court of Appeals) decisions using Bedesten API. - - İstinaf courts are intermediate appellate courts in the Turkish judicial system that handle - appeals from local civil courts before cases reach Yargıtay (Court of Cassation). - This is the only available tool for accessing İstinaf Hukuk Mahkemesi decisions. - - Key Features: - • Date range filtering with ISO 8601 format (YYYY-MM-DDTHH:MM:SS.000Z) - • Exact phrase search using double quotes: "\"legal term\"" - • Regular search for individual keywords - • Pagination support (1-100 results per page) - - Use cases: - • Research appellate court precedents - • Track appeals from specific lower courts - • Find decisions on specific legal issues at appellate level - • Analyze intermediate court reasoning before supreme court review - - Returns structured data with decision metadata including dates, case numbers, and summaries. - Use get_istinaf_hukuk_bedesten_document_markdown() to retrieve full decision texts. - """ - args = {"phrase": request.phrase, "pageSize": request.pageSize} - for field in ["kararTarihiStart", "kararTarihiEnd"]: - if getattr(request, field): - args[field] = getattr(request, field) - return await call_mcp_tool("search_istinaf_hukuk_bedesten", args) - -@app.get("/api/istinaf-hukuk/document/{document_id}", tags=["İstinaf Hukuk"]) -async def get_istinaf_hukuk_document(document_id: str): - """ - Retrieves the full text of an İstinaf Hukuk Mahkemesi decision document in Markdown format. - - This tool converts the original decision document (HTML or PDF) from Bedesten API - into clean, readable Markdown format suitable for analysis and processing. - - Input Requirements: - • documentId: Use the ID from search_istinaf_hukuk_bedesten results - • Document ID must be non-empty string - - Output Format: - • Clean Markdown text with proper formatting - • Preserves legal structure (headers, paragraphs, citations) - • Removes extraneous HTML/PDF artifacts - - Use for: - • Reading full appellate court decision texts - • Legal analysis of İstinaf court reasoning - • Citation extraction and reference building - • Content analysis and summarization - """ - return await call_mcp_tool("get_istinaf_hukuk_bedesten_document_markdown", {"documentId": document_id}) - -@app.post( - "/api/kyb/search", - tags=["KYB"], - summary="Search Extraordinary Appeals (KYB)", - description="""Search Kanun Yararına Bozma (Extraordinary Appeal) decisions. - -KYB is an extraordinary legal remedy where the Public Prosecutor's Office requests -review of finalized decisions in favor of law and defendants. Very rare but important -legal precedents. Only available tool for KYB decisions.""" -) -async def search_kyb(request: BedestenSearchRequest): - """ - Searches Kanun Yararına Bozma (KYB - Extraordinary Appeal) decisions using Bedesten API. - - KYB is an extraordinary legal remedy in the Turkish judicial system where the - Public Prosecutor's Office can request review of finalized decisions in favor of - the law and defendants. This is the only available tool for accessing KYB decisions. - - Key Features: - • Date range filtering with ISO 8601 format (YYYY-MM-DDTHH:MM:SS.000Z) - • Exact phrase search using double quotes: "\"extraordinary appeal\"" - • Regular search for individual keywords - • Pagination support (1-100 results per page) - - Legal Significance: - • Extraordinary remedy beyond regular appeals - • Initiated by Public Prosecutor's Office - • Reviews finalized decisions for legal errors - • Can benefit defendants retroactively - • Rare but important legal precedents - - Use cases: - • Research extraordinary appeal precedents - • Study prosecutorial challenges to final decisions - • Analyze legal errors in finalized cases - • Track KYB success rates and patterns - - Returns structured data with decision metadata. Use get_kyb_bedesten_document_markdown() - to retrieve full decision texts for detailed analysis. - """ - args = {"phrase": request.phrase, "pageSize": request.pageSize} - for field in ["kararTarihiStart", "kararTarihiEnd"]: - if getattr(request, field): - args[field] = getattr(request, field) - return await call_mcp_tool("search_kyb_bedesten", args) - -@app.get("/api/kyb/document/{document_id}", tags=["KYB"]) -async def get_kyb_document(document_id: str): - """ - Retrieves the full text of a Kanun Yararına Bozma (KYB) decision document in Markdown format. - - This tool converts the original extraordinary appeal decision document (HTML or PDF) - from Bedesten API into clean, readable Markdown format for analysis. - - Input Requirements: - • documentId: Use the ID from search_kyb_bedesten results - • Document ID must be non-empty string - - Output Format: - • Clean Markdown text with legal formatting preserved - • Structured content with headers and citations - • Removes technical artifacts from source documents - - Special Value for KYB Documents: - • Contains rare extraordinary appeal reasoning - • Shows prosecutorial arguments for legal review - • Documents correction of finalized legal errors - • Provides precedent for similar extraordinary circumstances - - Use for: - • Analyzing extraordinary appeal legal reasoning - • Understanding prosecutorial review criteria - • Research on legal error correction mechanisms - • Studying retroactive benefit applications - """ - return await call_mcp_tool("get_kyb_bedesten_document_markdown", {"documentId": document_id}) - -# ============================================================================ -# ADDITIONAL COURTS - 12 TOOLS -# ============================================================================ - -@app.post("/api/emsal/search", tags=["Emsal"], summary="Search UYAP Precedents") -async def search_emsal(request: EmsalSearchRequest): - """ - Searches for Precedent (Emsal) decisions using detailed criteria. - - The Precedent (Emsal) database contains precedent decisions from various Turkish courts - integrated through the UYAP (National Judiciary Informatics System). This tool provides - access to a comprehensive collection of court decisions that serve as legal precedents. - - Key Features: - • Multi-court coverage (BAM, Civil courts, Regional chambers) - • Keyword-based search across decision texts - • Court-specific filtering for targeted research - • Case number filtering (Case No (Esas No) and Decision No (Karar No) with ranges) - • Date range filtering with DD.MM.YYYY format - • Multiple sorting options and pagination support - - Court Selection Options: - • BAM Civil Courts: Higher regional civil courts - • Civil Courts: Local and first-instance civil courts - • Regional Civil Chambers: Specialized civil court departments - - Precedent Research Use Cases: - • Find precedent (emsal) decisions across multiple court levels - • Research court interpretations of specific legal concepts - • Analyze consistent legal reasoning patterns - • Study regional variations in legal decisions - • Track precedent development over time - • Compare decisions from different court types - - Returns structured precedent data with court information and decision metadata. - Use get_emsal_document_markdown() to retrieve full precedent decision texts. - """ - args = {"keyword": request.keyword, "results_per_page": request.results_per_page} - if request.decision_year_karar: - args["decision_year_karar"] = request.decision_year_karar - return await call_mcp_tool("search_emsal_detailed_decisions", args) - -@app.get("/api/emsal/document/{decision_id}", tags=["Emsal"]) -async def get_emsal_document(decision_id: str): - """ - Retrieves the full text of a specific Emsal (UYAP Precedent) decision in Markdown format. - - This tool fetches complete precedent decision documents from the UYAP system and converts - them to clean, readable Markdown format suitable for legal precedent analysis. - - Input Requirements: - • decision_id: Decision ID from search_emsal_detailed_decisions results - • ID must be non-empty string from UYAP Emsal database - - Output Format: - • Clean Markdown text with legal precedent structure preserved - • Organized sections: court info, case facts, legal reasoning, conclusion - • Proper formatting for legal citations and cross-references - • Removes technical artifacts from source documents - - Precedent Decision Content: - • Complete court reasoning and legal analysis - • Detailed examination of legal principles applied - • Citation of relevant laws, regulations, and prior precedents - • Final ruling with precedent-setting reasoning - • Court-specific interpretations and legal standards - - Use for legal precedent research, citation building, and comparative legal analysis. - """ - return await call_mcp_tool("get_emsal_document_markdown", {"decision_id": decision_id}) - -@app.post("/api/uyusmazlik/search", tags=["Uyuşmazlık"], summary="Search Jurisdictional Disputes") -async def search_uyusmazlik(request: UyusmazlikSearchRequest): - """ - Searches for Court of Jurisdictional Disputes (Uyuşmazlık Mahkemesi) decisions. - - The Court of Jurisdictional Disputes (Uyuşmazlık Mahkemesi) resolves jurisdictional disputes between different court systems - in Turkey, determining which court has jurisdiction over specific cases. This specialized - court handles conflicts between civil, criminal, and administrative jurisdictions. - - Key Features: - • Department filtering (Criminal, Civil, General Assembly decisions) - • Dispute type classification (Jurisdiction vs Judgment disputes) - • Decision outcome filtering (dispute resolution results) - • Case number and date range filtering - • Advanced text search with Boolean logic operators - • Official Gazette reference search - - Dispute Types: - • Jurisdictional Disputes (Görev Uyuşmazlığı): Which court has authority - • Judgment Disputes (Hüküm Uyuşmazlığı): Conflicting final decisions - - Departments: - • Criminal Section (Ceza Bölümü): Criminal section decisions - • Civil Section (Hukuk Bölümü): Civil section decisions - • General Assembly Decisions (Genel Kurul Kararları): General Assembly decisions - - Use cases: - • Research jurisdictional precedents - • Understand court system boundaries - • Analyze dispute resolution patterns - • Study inter-court conflict resolution - • Legal procedure and jurisdiction research - - Returns structured search results with dispute resolution information. - """ - return await call_mcp_tool("search_uyusmazlik_decisions", { - "keywords": request.keywords, - "page_to_fetch": request.page_to_fetch - }) - -@app.get("/api/uyusmazlik/document", tags=["Uyuşmazlık"]) -async def get_uyusmazlik_document(document_url: str): - """ - Retrieves the full text of a specific Uyuşmazlık Mahkemesi decision from its URL in Markdown format. - - This tool fetches complete jurisdictional dispute resolution decisions and converts them - to clean, readable Markdown format suitable for legal analysis of inter-court disputes. - - Input Requirements: - • document_url: Full URL to the decision document from search_uyusmazlik_decisions results - • URL must be valid HttpUrl format from official Uyuşmazlık Mahkemesi database - - Output Format: - • Clean Markdown text with jurisdictional dispute structure preserved - • Organized sections: dispute facts, jurisdictional analysis, resolution ruling - • Proper formatting for legal citations and court system references - • Removes technical artifacts from source documents - - Jurisdictional Dispute Decision Content: - • Complete analysis of jurisdictional conflicts between court systems - • Detailed examination of applicable jurisdictional rules - • Citation of relevant procedural laws and court organization statutes - • Final resolution determining proper court jurisdiction - • Reasoning for jurisdictional boundaries and court authority - - Use for understanding court system boundaries, analyzing jurisdictional precedents, - and legal practice guidance on proper court selection. - """ - return await call_mcp_tool("get_uyusmazlik_document_markdown_from_url", {"document_url": document_url}) - -@app.post("/api/anayasa/search-norm", tags=["Anayasa"], summary="Search Constitutional Court (Norm Control)") -async def search_anayasa_norm(request: AnayasaNormSearchRequest): - """ - Searches Constitutional Court (Anayasa Mahkemesi) norm control decisions with comprehensive filtering. - - The Constitutional Court is Turkey's highest constitutional authority, responsible for judicial - review of laws, regulations, and constitutional amendments. Norm control (Norm Denetimi) is the - court's power to review the constitutionality of legal norms. - - Key Features: - • Boolean keyword search (AND, OR, NOT logic) - • Constitutional period filtering (1961 vs 1982 Constitution) - • Case and decision number filtering - • Date range filtering for review and decision dates - • Application type classification (İptal, İtiraz, etc.) - • Applicant filtering (government entities, opposition parties) - • Official Gazette publication filtering - • Judicial opinion analysis (dissents, different reasoning) - • Court member and rapporteur filtering - • Norm type classification (laws, regulations, decrees) - • Review outcome filtering (constitutionality determinations) - • Constitutional basis article referencing - - Constitutional Review Types: - • Abstract review: Ex ante constitutional control - • Concrete review: Constitutional questions during litigation - • Legislative review: Parliamentary acts and government decrees - • Regulatory review: Administrative regulations and bylaws - - Use cases: - • Constitutional law research and analysis - • Legislative drafting constitutional compliance - • Academic constitutional law study - • Legal precedent analysis for constitutional questions - • Government policy constitutional assessment - - Returns structured constitutional court data with comprehensive metadata. - """ - args = {"keywords_all": request.keywords_all, "results_per_page": request.results_per_page} - for field in ["period", "application_type"]: - if getattr(request, field): - args[field] = getattr(request, field) - return await call_mcp_tool("search_anayasa_norm_denetimi_decisions", args) - -@app.get("/api/anayasa/norm-document", tags=["Anayasa"]) -async def get_anayasa_norm_document(document_url: str, page_number: int = 1): - """Get Constitutional Court norm control decision in paginated Markdown format.""" - return await call_mcp_tool("get_anayasa_norm_denetimi_document_markdown", { - "document_url": document_url, "page_number": page_number - }) - -@app.post("/api/anayasa/search-bireysel", tags=["Anayasa"], summary="Search Constitutional Court (Individual Applications)") -async def search_anayasa_bireysel(request: AnayasaBireyselSearchRequest): - """ - Search Constitutional Court individual application (Bireysel Başvuru) decisions for human rights violation reports with keyword filtering. - - Individual applications allow citizens to petition the Constitutional Court directly for - violations of fundamental rights and freedoms. This tool generates decision search reports - that help identify relevant human rights violation cases. - - Key Features: - • Keyword-based search with AND logic - • Human rights violation case identification - • Individual petition decision analysis - • Fundamental rights and freedoms research - • Pagination support for large result sets - - Individual Application System: - • Direct citizen access to Constitutional Court - • Human rights and fundamental freedoms protection - • Alternative to European Court of Human Rights - • Domestic remedy for constitutional violations - • Individual justice and rights enforcement - - Human Rights Categories: - • Right to life and personal liberty - • Right to fair trial and due process - • Freedom of expression and press - • Freedom of religion and conscience - • Property rights and economic freedoms - • Right to privacy and family life - • Political rights and democratic participation - - Use cases: - • Human rights violation research - • Individual petition precedent analysis - • Constitutional rights interpretation study - • Legal remedies for rights violations - • Academic human rights law research - • Civil society and NGO legal research - - Returns search report with case summaries and violation categories. - Use get_anayasa_bireysel_document for full decision texts. - """ - return await call_mcp_tool("search_anayasa_bireysel_basvuru_report", { - "keywords": request.keywords, "page_to_fetch": request.page_to_fetch - }) - -@app.get("/api/anayasa/bireysel-document", tags=["Anayasa"]) -async def get_anayasa_bireysel_document(document_url: str, page_number: int = 1): - """ - Retrieve the full text of a Constitutional Court individual application decision in paginated Markdown format. - - This tool fetches complete human rights violation decisions from individual applications - and converts them to clean, readable Markdown format. Content is paginated into - 5,000-character chunks for easier processing. - - Input Requirements: - • document_url: URL path (e.g., /BB/YYYY/NNNN) from search results - • page_number: Page number for pagination (1-indexed, default: 1) - - Output Format: - • Clean Markdown text with human rights case structure preserved - • Organized sections: applicant info, violation claims, court analysis, ruling - • Proper formatting for human rights law citations and references - • Paginated content with navigation information - - Individual Application Decision Content: - • Complete human rights violation analysis - • Detailed examination of fundamental rights claims - • Citation of constitutional articles and international human rights law - • Final determination on rights violations with remedies - • Analysis of domestic court proceedings and their adequacy - • Individual remedy recommendations and compensation - - Use for: - • Reading full human rights violation decisions - • Human rights law research and precedent analysis - • Understanding constitutional rights protection standards - • Individual petition strategy development - • Academic human rights and constitutional law study - • Civil society monitoring of rights violations - """ - return await call_mcp_tool("get_anayasa_bireysel_basvuru_document_markdown", { - "document_url": document_url, "page_number": page_number - }) - -@app.post("/api/kik/search", tags=["KİK"], summary="Search Public Procurement Authority") -async def search_kik(request: KikSearchRequest): - """ - Search Public Procurement Authority (Kamu İhale Kurulu - KIK) decisions with comprehensive filtering for public procurement law and administrative dispute research. - - The Public Procurement Authority (KIK) is Turkey's procurement oversight body, responsible for - regulating public procurement processes, resolving procurement disputes, and issuing interpretive - decisions on public contracting laws. This tool provides access to official procurement decisions - and regulatory guidance. - - Key Features: - • Decision type filtering (Disputes, Regulatory, Court decisions) - • Decision date range filtering for temporal analysis - • Applicant and procuring entity filtering - • Tender subject and content-based search - • Decision number and reference tracking - • Comprehensive metadata extraction - - Public Procurement Decision Types: - • Uyuşmazlık Kararları: Dispute resolution decisions - • Düzenleyici Kararlar: Regulatory and interpretive decisions - • Mahkeme Kararları: Court decisions and judicial precedents - - Procurement Law Areas: - • Tender procedure compliance and violations - • Bid evaluation and award criteria disputes - • Contractor qualification and eligibility - • Contract modification and scope changes - • Performance guarantees and penalty applications - • Public procurement ethics and transparency - • Emergency procurement and exceptional procedures - - Use cases: - • Public procurement law research and compliance guidance - • Tender dispute resolution precedent analysis - • Government contracting risk assessment - • Procurement policy and regulatory interpretation - • Academic public administration and law study - • Legal strategy development for procurement disputes - - Returns structured procurement authority data with comprehensive case metadata. - Use get_kik_document for full decision texts with detailed reasoning. - """ - args = {} - for field in ["karar_tipi", "karar_metni", "basvuru_konusu_ihale", "karar_tarihi_baslangic"]: - if getattr(request, field): - args[field] = getattr(request, field) - return await call_mcp_tool("search_kik_decisions", args) - -@app.get("/api/kik/document/{decision_id}", tags=["KİK"]) -async def get_kik_document(decision_id: str): - """ - Retrieve the full text of a Public Procurement Authority (KIK) decision in paginated Markdown format. - - This tool fetches complete public procurement decisions and converts them from PDF to clean, - readable Markdown format. Content is paginated into manageable chunks for processing lengthy - procurement law decisions and regulatory interpretations. - - Input Requirements: - • decision_id: KIK decision ID (base64 encoded karar_id) from search results - • Decision ID must be non-empty string - - Output Format: - • Clean Markdown text converted from original PDF documents - • Organized sections: case summary, legal analysis, regulatory interpretation, decision - • Proper formatting for procurement law citations and regulatory references - • Paginated content with navigation information - • Metadata including PDF source link and document information - - Public Procurement Decision Content: - • Complete procurement dispute analysis and resolution - • Detailed examination of tender procedures and compliance - • Citation of procurement laws, regulations, and precedents - • Final determination on procurement violations with corrective measures - • Regulatory guidance and policy interpretations - • Contractor liability and penalty determinations - - Use for: - • Reading full public procurement authority decisions - • Procurement law research and precedent analysis - • Government contracting compliance and risk assessment - • Tender dispute resolution strategy development - • Academic public administration and procurement law study - • Policy analysis and regulatory interpretation - """ - return await call_mcp_tool("get_kik_document_markdown", {"decision_id": decision_id}) - -@app.post("/api/rekabet/search", tags=["Rekabet"], summary="Search Competition Authority") -async def search_rekabet(request: RekabetSearchRequest): - """ - Search Competition Authority (Rekabet Kurumu) decisions with comprehensive filtering for competition law and antitrust research. - - The Competition Authority (Rekabet Kurumu) is Turkey's competition authority, responsible for enforcing antitrust laws, - preventing anti-competitive practices, and regulating mergers and acquisitions. This tool - provides access to official competition law decisions and regulatory interpretations. - - Key Features: - • Decision type filtering (Mergers, Violations, Exemptions, etc.) - • Title and content-based text search with exact phrase matching - • Publication date filtering - • Case year and decision number filtering - • Pagination support for large result sets - - Competition Law Decision Types: - • Birleşme ve Devralma: Merger and acquisition approvals - • Rekabet İhlali: Competition violation investigations - • Muafiyet: Exemption and negative clearance decisions - • Geçici Tedbir: Interim measures and emergency orders - • Sektör İncelemesi: Sector inquiry and market studies - • Diğer: Other regulatory and interpretive decisions - - Competition Law Areas: - • Anti-competitive agreements and cartels - • Abuse of dominant market position - • Merger control and market concentration - • Vertical agreements and distribution restrictions - • Unfair competition and consumer protection - • Market definition and economic analysis - - Advanced Search: - • Exact phrase matching with double quotes for precise legal terms - • Content search across full decision texts (PdfText parameter) - • Title search for specific case names or topics - • Date range filtering for temporal analysis - - Example for exact phrase search: PdfText=\"tender process\" consultancy - - Use cases: - • Competition law research and precedent analysis - • Merger and acquisition due diligence - • Antitrust compliance and risk assessment - • Market analysis and competitive intelligence - • Academic competition economics study - • Legal strategy development for competition cases - - Returns structured competition authority data with comprehensive metadata. - Use get_rekabet_document for full decision texts (paginated PDF conversion). - """ - args = {"page": request.page} - for field in ["KararTuru", "PdfText", "YayinlanmaTarihi"]: - if getattr(request, field): - args[field] = getattr(request, field) - return await call_mcp_tool("search_rekabet_kurumu_decisions", args) - -@app.get("/api/rekabet/document/{karar_id}", tags=["Rekabet"]) -async def get_rekabet_document(karar_id: str, page_number: int = 1): - """ - Retrieve the full text of a Competition Authority (Rekabet Kurumu) decision in paginated Markdown format converted from PDF. - - This tool fetches complete competition authority decisions and converts them from PDF to clean, - readable Markdown format. Content is paginated for easier processing of lengthy competition law decisions. - - Input Requirements: - • karar_id: GUID (kararId) from search_rekabet_kurumu_decisions results - • page_number: Page number for pagination (1-indexed, default: 1) - - Output Format: - • Clean Markdown text converted from original PDF documents - • Organized sections: case summary, market analysis, legal reasoning, decision - • Proper formatting for competition law citations and references - • Paginated content with navigation information - • Metadata including PDF source link and document information - - Competition Authority Decision Content: - • Complete competition law analysis and market assessment - • Detailed examination of anti-competitive practices - • Economic analysis and market definition studies - • Citation of competition laws, regulations, and precedents - • Final determination on competition violations with remedies - • Merger and acquisition approval conditions - • Regulatory guidance and policy interpretations - - Use for: - • Reading full competition authority decisions - • Competition law research and precedent analysis - • Market analysis and economic impact assessment - • Antitrust compliance and risk evaluation - • Academic competition economics and law study - • Legal strategy development for competition cases - """ - return await call_mcp_tool("get_rekabet_kurumu_document", { - "karar_id": karar_id, "page_number": page_number - }) - -# ============================================================================ -# SAYISTAY (COURT OF ACCOUNTS) ENDPOINTS - 6 TOOLS -# ============================================================================ - -@app.post("/api/sayistay/search-genel-kurul", tags=["Sayıştay"], summary="Search Court of Accounts (General Assembly)") -async def search_sayistay_genel_kurul(request: SayistaySearchRequest): - """ - Search Sayıştay Genel Kurul (General Assembly) decisions - highest-level audit precedents and interpretive rulings with keyword-based filtering. - - The General Assembly represents the highest decision-making body of Turkey's Court of Accounts, - issuing authoritative interpretations of audit laws, precedential rulings on complex accountability - issues, and binding guidance for audit practice. These decisions establish fundamental principles - for public financial oversight and accountability standards. - - Key Features: - • Keyword-based search with AND logic for precise case finding - • Highest-level audit precedent identification - • Interpretive ruling analysis and legal guidance extraction - • Complex accountability issue resolution tracking - • Pagination support for comprehensive result sets - - General Assembly Decision Authority: - • Ultimate audit law interpretation and clarification - • Precedential rulings binding on all audit chambers - • Complex inter-chamber jurisdiction dispute resolution - • Policy guidance for audit methodology and standards - • Final determination on constitutional audit questions - - Public Audit Areas: - • Government budget execution and compliance - • Public institution financial management - • State-owned enterprise oversight and accountability - • Local government and municipal audit standards - • Public procurement oversight and compliance monitoring - • Performance audit methodology and effectiveness standards - • Public revenue collection and tax administration audit - - Use Cases: - • Research authoritative audit law interpretations - • Find binding precedents for complex audit questions - • Study evolution of public accountability standards - • Analyze audit methodology development and refinement - • Academic public administration and audit research - • Government policy and accountability framework analysis - - Returns structured General Assembly data with comprehensive legal precedent metadata. - Use get_sayistay_genel_kurul_document for full decision texts with detailed reasoning. - """ - return await call_mcp_tool("search_sayistay_genel_kurul", { - "keywords": request.keywords, "page_to_fetch": request.page_to_fetch - }) - -@app.get("/api/sayistay/genel-kurul-document", tags=["Sayıştay"]) -async def get_sayistay_genel_kurul_document(document_url: str, page_number: int = 1): - """ - Retrieve the full text of a Sayıştay Genel Kurul decision document in Markdown format for detailed analysis. - - This tool converts the original General Assembly decision document into clean, - readable Markdown format suitable for legal analysis and research. - - Input Requirements: - • decision_id: Use the ID from search_sayistay_genel_kurul results - • Decision ID must be non-empty string - - Output Format: - • Clean Markdown text with legal formatting preserved - • Structured content with reasoning and conclusions - • Removes technical artifacts from source documents - - Use for: - • Detailed analysis of audit precedents - • Research on public accountability standards - • Citation and reference building - • Legal interpretation and case study development - """ - return await call_mcp_tool("get_sayistay_genel_kurul_document_markdown", { - "document_url": document_url, "page_number": page_number - }) - -@app.post("/api/sayistay/search-temyiz-kurulu", tags=["Sayıştay"], summary="Search Court of Accounts (Appeals Board)") -async def search_sayistay_temyiz_kurulu(request: SayistaySearchRequest): - """ - Search Sayıştay Temyiz Kurulu (Appeals Board) decisions - appellate review of audit chamber findings with advanced filtering and institutional analysis. - - The Appeals Board serves as the intermediate appellate authority in Turkey's audit system, - reviewing first-instance chamber decisions on audit findings, liability determinations, - and sanctions. Appeals Board decisions refine audit standards and ensure consistency - across different audit chambers. - - Key Features: - • Chamber-specific filtering for targeted appeals analysis - • Institutional type categorization for audit pattern analysis - • Decision and account year filtering for temporal trends - • Audit subject matter classification and content search - • Appeal outcome tracking and precedent identification - • Pagination support for comprehensive coverage - - Appeals Board Authority: - • Review and modification of chamber audit findings - • Standardization of audit liability determinations - • Consistency enforcement across audit chambers - • Intermediate precedent development for audit practice - • Quality control for first-instance audit decisions - - Audit Review Areas: - • Government agency financial accountability appeals - • Municipality and local government audit review - • State enterprise oversight and performance audit appeals - • Educational institution audit finding review - • Healthcare system financial accountability appeals - • Public procurement oversight and compliance review - • Tax administration and revenue audit appeals - - Use Cases: - • Research audit appeals patterns and outcomes - • Study chamber decision consistency and standards - • Analyze audit liability determination evolution - • Find precedents for specific audit finding types - • Track institutional audit patterns and compliance - • Academic public accountability and audit law research - - Returns structured Appeals Board data with comprehensive appellate analysis metadata. - Use get_sayistay_temyiz_kurulu_document for full appeals decisions with detailed reasoning. - """ - return await call_mcp_tool("search_sayistay_temyiz_kurulu", { - "keywords": request.keywords, "page_to_fetch": request.page_to_fetch - }) - -@app.get("/api/sayistay/temyiz-kurulu-document", tags=["Sayıştay"]) -async def get_sayistay_temyiz_kurulu_document(document_url: str, page_number: int = 1): - """ - Retrieve the full text of a Sayıştay Temyiz Kurulu decision document in Markdown format for detailed appeals analysis. - - This tool converts the original Appeals Board decision document into clean, - readable Markdown format for analysis of appellate reasoning and standards. - - Input Requirements: - • decision_id: Use the ID from search_sayistay_temyiz_kurulu results - • Decision ID must be non-empty string - - Output Format: - • Clean Markdown text with appellate reasoning preserved - • Structured content with original findings and appeals analysis - • Removes technical artifacts from source documents - - Use for: - • Analysis of appeals board reasoning and standards - • Research on audit liability determination evolution - • Understanding chamber decision review criteria - • Precedent analysis for audit appeal cases - """ - return await call_mcp_tool("get_sayistay_temyiz_kurulu_document_markdown", { - "document_url": document_url, "page_number": page_number - }) - -@app.post("/api/sayistay/search-daire", tags=["Sayıştay"], summary="Search Court of Accounts (Chambers)") -async def search_sayistay_daire(request: SayistaySearchRequest): - """ - Search Sayıştay Daire (Chamber) decisions - first-instance audit findings and sanctions from individual audit chambers with comprehensive filtering and subject categorization. - - Chamber decisions represent first-instance audit findings, sanctions, and - liability determinations issued by specialized audit chambers. These form - the foundation of Turkey's public financial accountability system. - - Key Features: - • Chamber-specific filtering (8 specialized audit chambers) - • Decision and account year filtering (2012-2025) - • Public administration type categorization - • Subject matter classification and full-text search - • Audit report tracking and institutional analysis - - Use Cases: - • Research specific audit findings and sanctions - • Study chamber specialization and jurisdiction - • Analyze audit patterns by institution type - • Find precedents for financial irregularities - • Track audit evolution across fiscal years - """ - return await call_mcp_tool("search_sayistay_daire", { - "keywords": request.keywords, "page_to_fetch": request.page_to_fetch - }) - -@app.get("/api/sayistay/daire-document", tags=["Sayıştay"]) -async def get_sayistay_daire_document(document_url: str, page_number: int = 1): - """ - Retrieve the full text of a Sayıştay Daire decision document in Markdown format for detailed audit findings analysis. - - This tool converts the original chamber decision document into clean, - readable Markdown format for analysis of first-instance audit findings. - - Input Requirements: - • decision_id: Use the ID from search_sayistay_daire results - • Decision ID must be non-empty string - - Output Format: - • Clean Markdown text with audit findings preserved - • Structured content with violations and sanctions - • Removes technical artifacts from source documents - - Use for: - • Detailed analysis of audit findings and methodology - • Research on specific types of financial irregularities - • Understanding chamber jurisdiction and specialization - • Case study development for audit training and compliance - """ - return await call_mcp_tool("get_sayistay_daire_document_markdown", { - "document_url": document_url, "page_number": page_number - }) - -# ============================================================================ -# ADDITIONAL API ENDPOINTS -# ============================================================================ - -@app.get("/api/databases") -async def list_databases(): - """Comprehensive database information with tool mappings""" - return { - "total_tools": 33, - "databases": { - "yargitay": { - "name": "Yargıtay (Court of Cassation)", - "description": "Turkey's Supreme Court for civil and criminal matters", - "tools": 4, - "chambers": 52, - "search_tools": ["search_yargitay", "search_yargitay_bedesten"], - "document_tools": ["get_yargitay_document", "get_yargitay_bedesten_document"] - }, - "danistay": { - "name": "Danıştay (Council of State)", - "description": "Turkey's Supreme Administrative Court", - "tools": 5, - "chambers": 27, - "search_tools": ["search_danistay_keyword", "search_danistay_detailed", "search_danistay_bedesten"], - "document_tools": ["get_danistay_document", "get_danistay_bedesten_document"] - }, - "anayasa": { - "name": "Anayasa Mahkemesi (Constitutional Court)", - "description": "Constitutional review and individual applications", - "tools": 4, - "features": ["norm_control", "individual_applications", "human_rights"] - }, - "rekabet": { - "name": "Rekabet Kurumu (Competition Authority)", - "description": "Antitrust and merger control", - "tools": 2, - "coverage": ["mergers", "cartels", "market_abuse", "sector_inquiries"] - }, - "kik": { - "name": "Kamu İhale Kurulu (Public Procurement Authority)", - "description": "Government procurement disputes", - "tools": 2, - "coverage": ["procurement_disputes", "regulatory_decisions", "tender_violations"] - }, - "sayistay": { - "name": "Sayıştay (Court of Accounts)", - "description": "Public audit and financial accountability", - "tools": 6, - "levels": ["general_assembly", "appeals_board", "audit_chambers"] - }, - "emsal": { - "name": "Emsal (UYAP Precedents)", - "description": "Cross-court precedent database", - "tools": 2, - "coverage": ["multi_court", "precedent_analysis"] - }, - "uyusmazlik": { - "name": "Uyuşmazlık Mahkemesi (Jurisdictional Disputes)", - "description": "Inter-court jurisdiction disputes", - "tools": 2, - "specialization": "jurisdictional_conflicts" - }, - "bedesten_courts": { - "name": "Bedesten API Courts (Local/Appellate/KYB)", - "description": "First instance, appellate, and extraordinary appeal courts", - "tools": 6, - "courts": ["yerel_hukuk", "istinaf_hukuk", "kyb"], - "coverage": "complete_court_hierarchy" - } - } - } - -@app.get("/api/stats") -async def get_statistics(): - """Comprehensive API statistics and capabilities""" - uptime = (datetime.now() - SERVER_START_TIME).total_seconds() - return { - "server": { - "uptime_seconds": uptime, - "start_time": SERVER_START_TIME.isoformat(), - "version": "1.0.0", - "status": "operational" - }, - "coverage": { - "total_tools": 33, - "total_databases": 9, - "total_chambers": 79, # 52 Yargıtay + 27 Danıştay - "search_tools": 16, - "document_tools": 17 - }, - "capabilities": { - "advanced_search_operators": True, - "exact_phrase_search": True, - "date_range_filtering": True, - "chamber_filtering": True, - "boolean_logic": True, - "wildcard_search": True, - "pagination": True, - "markdown_conversion": True, - "pdf_processing": True, - "dual_api_support": True - }, - "legal_coverage": { - "supreme_courts": ["Yargıtay", "Danıştay"], - "constitutional_law": "Anayasa Mahkemesi", - "administrative_law": "Full coverage", - "competition_law": "Rekabet Kurumu", - "public_procurement": "KİK", - "public_audit": "Sayıştay", - "court_hierarchy": "Complete (Local → Appellate → Supreme)", - "specialized_courts": ["Uyuşmazlık", "Constitutional", "Administrative"] - } - } - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/client.py deleted file mode 100644 index 5a3d51a..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/client.py +++ /dev/null @@ -1,1199 +0,0 @@ -# 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 io -import random - -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() - # Ultra stealth browser configuration - self.browser = await self.playwright_instance.chromium.launch( - headless=True, - args=[ - # Disable automation indicators - '--no-first-run', - '--no-default-browser-check', - '--disable-dev-shm-usage', - '--disable-extensions', - '--disable-gpu', - '--disable-default-apps', - '--disable-translate', - '--disable-blink-features=AutomationControlled', - '--disable-ipc-flooding-protection', - '--disable-renderer-backgrounding', - '--disable-backgrounding-occluded-windows', - '--disable-client-side-phishing-detection', - '--disable-sync', - '--disable-features=TranslateUI,BlinkGenPropertyTrees', - '--disable-component-extensions-with-background-pages', - '--no-sandbox', # Sometimes needed for headless - '--disable-web-security', - '--disable-features=VizDisplayCompositor', - # Language and locale - '--lang=tr-TR', - '--accept-lang=tr-TR,tr;q=0.9,en;q=0.8', - # Performance optimizations - '--memory-pressure-off', - '--max_old_space_size=4096', - ] - ) - 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.") - # Ultra realistic context configuration - 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/131.0.0.0 Safari/537.36", - viewport={'width': 1920, 'height': 1080}, - screen={'width': 1920, 'height': 1080}, - device_scale_factor=1.0, - is_mobile=False, - has_touch=False, - # Localization - locale='tr-TR', - timezone_id='Europe/Istanbul', - # Realistic browser features - java_script_enabled=True, - accept_downloads=True, - ignore_https_errors=True, - # Color scheme and media - color_scheme='light', - reduced_motion='no-preference', - forced_colors='none', - # Additional headers for realism - extra_http_headers={ - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': 'tr-TR,tr;q=0.9,en;q=0.8', - 'Cache-Control': 'max-age=0', - 'DNT': '1', - 'Upgrade-Insecure-Requests': '1', - 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', - 'Sec-Ch-Ua-Mobile': '?0', - 'Sec-Ch-Ua-Platform': '"Windows"', - 'Sec-Fetch-Dest': 'document', - 'Sec-Fetch-Mode': 'navigate', - 'Sec-Fetch-Site': 'none', - 'Sec-Fetch-User': '?1', - }, - # Permissions to appear realistic - permissions=['geolocation'], - geolocation={'latitude': 41.0082, 'longitude': 28.9784}, # Istanbul - ) - 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) - - # CRITICAL: Anti-detection JavaScript injection - await self._inject_stealth_scripts() - 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.") - - async def _inject_stealth_scripts(self): - """ - Inject comprehensive stealth JavaScript to evade bot detection. - Overrides navigator properties and other fingerprinting vectors. - """ - if not self.page: - logger.warning("Cannot inject stealth scripts: page is None") - return - - logger.debug("Injecting comprehensive stealth scripts...") - - stealth_script = ''' - // Override navigator.webdriver - Object.defineProperty(navigator, 'webdriver', { - get: () => undefined, - configurable: true - }); - - // Override navigator properties to appear more human - Object.defineProperty(navigator, 'languages', { - get: () => ['tr-TR', 'tr', 'en-US', 'en'], - configurable: true - }); - - Object.defineProperty(navigator, 'platform', { - get: () => 'Win32', - configurable: true - }); - - Object.defineProperty(navigator, 'vendor', { - get: () => 'Google Inc.', - configurable: true - }); - - Object.defineProperty(navigator, 'deviceMemory', { - get: () => 8, - configurable: true - }); - - Object.defineProperty(navigator, 'hardwareConcurrency', { - get: () => 8, - configurable: true - }); - - Object.defineProperty(navigator, 'maxTouchPoints', { - get: () => 0, - configurable: true - }); - - // Override plugins to appear realistic - Object.defineProperty(navigator, 'plugins', { - get: () => { - return [ - { - 0: {type: "application/x-google-chrome-pdf", suffixes: "pdf", description: "Portable Document Format", enabledPlugin: Plugin}, - description: "Portable Document Format", - filename: "internal-pdf-viewer", - length: 1, - name: "Chrome PDF Plugin" - }, - { - 0: {type: "application/pdf", suffixes: "pdf", description: "", enabledPlugin: Plugin}, - description: "", - filename: "mhjfbmdgcfjbbpaeojofohoefgiehjai", - length: 1, - name: "Chrome PDF Viewer" - } - ]; - }, - configurable: true - }); - - // Override permissions - const originalQuery = window.navigator.permissions.query; - window.navigator.permissions.query = (parameters) => ( - parameters.name === 'notifications' ? - Promise.resolve({ state: Notification.permission }) : - originalQuery(parameters) - ); - - // Override WebGL rendering context - const getParameter = WebGLRenderingContext.prototype.getParameter; - WebGLRenderingContext.prototype.getParameter = function(parameter) { - if (parameter === 37445) { // UNMASKED_VENDOR_WEBGL - return 'Intel Inc.'; - } - if (parameter === 37446) { // UNMASKED_RENDERER_WEBGL - return 'Intel(R) Iris(R) Plus Graphics 640'; - } - return getParameter(parameter); - }; - - // Override canvas fingerprinting - const toBlob = HTMLCanvasElement.prototype.toBlob; - const toDataURL = HTMLCanvasElement.prototype.toDataURL; - const getImageData = CanvasRenderingContext2D.prototype.getImageData; - - const noisify = (canvas, context) => { - const imageData = context.getImageData(0, 0, canvas.width, canvas.height); - for (let i = 0; i < imageData.data.length; i += 4) { - imageData.data[i] += Math.floor(Math.random() * 10) - 5; - imageData.data[i + 1] += Math.floor(Math.random() * 10) - 5; - imageData.data[i + 2] += Math.floor(Math.random() * 10) - 5; - } - context.putImageData(imageData, 0, 0); - }; - - Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { - value: function(callback, type, encoderOptions) { - noisify(this, this.getContext('2d')); - return toBlob.apply(this, arguments); - } - }); - - Object.defineProperty(HTMLCanvasElement.prototype, 'toDataURL', { - value: function(type, encoderOptions) { - noisify(this, this.getContext('2d')); - return toDataURL.apply(this, arguments); - } - }); - - // Override AudioContext for audio fingerprinting - const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - const originalAnalyser = audioCtx.createAnalyser; - audioCtx.createAnalyser = function() { - const analyser = originalAnalyser.apply(this, arguments); - const getFloatFrequencyData = analyser.getFloatFrequencyData; - analyser.getFloatFrequencyData = function(array) { - getFloatFrequencyData.apply(this, arguments); - for (let i = 0; i < array.length; i++) { - array[i] += Math.random() * 0.0001; - } - }; - return analyser; - }; - - // Override screen properties - Object.defineProperty(window.screen, 'colorDepth', { - get: () => 24, - configurable: true - }); - - Object.defineProperty(window.screen, 'pixelDepth', { - get: () => 24, - configurable: true - }); - - // Override timezone - Date.prototype.getTimezoneOffset = function() { - return -180; // UTC+3 (Istanbul) - }; - - // Override document.cookie to prevent tracking - const originalCookieDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie') || - Object.getOwnPropertyDescriptor(HTMLDocument.prototype, 'cookie'); - if (originalCookieDescriptor && originalCookieDescriptor.configurable) { - Object.defineProperty(document, 'cookie', { - get: function() { - return originalCookieDescriptor.get.call(this); - }, - set: function(val) { - console.log('Cookie set blocked:', val); - return originalCookieDescriptor.set.call(this, val); - }, - configurable: true - }); - } - - // Remove automation traces - delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array; - delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise; - delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol; - delete window.cdc_adoQpoasnfa76pfcZLmcfl_JSON; - delete window.cdc_adoQpoasnfa76pfcZLmcfl_Object; - delete window.cdc_adoQpoasnfa76pfcZLmcfl_Proxy; - - // Add realistic performance timing - if (window.performance && window.performance.timing) { - const timing = window.performance.timing; - const now = Date.now(); - Object.defineProperty(timing, 'navigationStart', { value: now - Math.floor(Math.random() * 1000) + 1000, configurable: false }); - Object.defineProperty(timing, 'loadEventEnd', { value: now - Math.floor(Math.random() * 100) + 100, configurable: false }); - } - - console.log('✓ Stealth scripts injected successfully'); - ''' - - try: - await self.page.add_init_script(stealth_script) - logger.debug("✅ Stealth scripts injected successfully") - except Exception as e: - logger.warning(f"⚠️ Failed to inject stealth scripts: {e}") - - async def _simulate_human_behavior(self, fast_mode: bool = True): - """ - Simulate realistic human behavior patterns to avoid detection. - Includes mouse movements, typing patterns, and natural delays. - - Args: - fast_mode: If True, use minimal timing for speed optimization - """ - if not self.page: - logger.warning("Cannot simulate human behavior: page is None") - return - - logger.debug("🤖 Simulating human behavior patterns...") - - try: - if fast_mode: - # ULTRA-FAST MODE: Minimal human behavior - viewport_size = self.page.viewport_size - if viewport_size and random.random() < 0.7: # 70% chance to do movement - width, height = viewport_size['width'], viewport_size['height'] - - # Single quick mouse movement - x = random.randint(200, width - 200) - y = random.randint(200, height - 200) - await self.page.mouse.move(x, y) - - # Brief scroll (50% chance) - if random.random() < 0.5: - await self.page.mouse.wheel(0, random.randint(50, 100)) - - # Ultra-minimal delay - await asyncio.sleep(random.uniform(0.05, 0.15)) # Reduced from 0.1-0.3 - - else: - # FULL MODE: Original comprehensive behavior - viewport_size = self.page.viewport_size - if viewport_size: - width, height = viewport_size['width'], viewport_size['height'] - - # Generate 3-5 random mouse movements - movements = random.randint(3, 5) - logger.debug(f" 🖱️ Performing {movements} random mouse movements") - - for i in range(movements): - x = random.randint(100, width - 100) - y = random.randint(100, height - 100) - - # Move mouse with realistic speed (not instant) - await self.page.mouse.move(x, y) - await asyncio.sleep(random.uniform(0.1, 0.3)) - - # 2. Scroll simulation - logger.debug(" 📜 Simulating scroll behavior") - scroll_amount = random.randint(100, 300) - await self.page.mouse.wheel(0, scroll_amount) - await asyncio.sleep(random.uniform(0.2, 0.5)) - - # Scroll back up - await self.page.mouse.wheel(0, -scroll_amount) - await asyncio.sleep(random.uniform(0.2, 0.4)) - - # 3. Random page interaction delays - await asyncio.sleep(random.uniform(0.5, 1.5)) - - logger.debug("✅ Human behavior simulation completed") - - except Exception as e: - logger.warning(f"⚠️ Human behavior simulation failed: {e}") - - async def _human_type(self, selector: str, text: str, clear_first: bool = True, fast_mode: bool = True): - """ - Type text with human-like patterns and delays. - - Args: - selector: CSS selector for the input element - text: Text to type - clear_first: Whether to clear the field first - fast_mode: If True, use minimal delays for speed optimization - """ - if not self.page: - logger.warning("Cannot perform human typing: page is None") - return - - try: - if fast_mode: - # FAST MODE: Direct fill for speed - await self.page.fill(selector, text) - await asyncio.sleep(random.uniform(0.02, 0.05)) # Reduced from 0.05-0.1 - else: - # FULL MODE: Character-by-character human typing - # Focus on the element first - await self.page.focus(selector) - await asyncio.sleep(random.uniform(0.1, 0.3)) - - # Clear field if requested - if clear_first: - await self.page.keyboard.press('Control+a') - await asyncio.sleep(random.uniform(0.05, 0.15)) - await self.page.keyboard.press('Delete') - await asyncio.sleep(random.uniform(0.05, 0.15)) - - # Type each character with human-like delays - for char in text: - await self.page.keyboard.type(char) - # Human typing speed: 50-150ms between characters - delay = random.uniform(0.05, 0.15) - - # Occasional longer pauses (thinking) - if random.random() < 0.1: # 10% chance - delay += random.uniform(0.2, 0.8) - - await asyncio.sleep(delay) - - # Brief pause after typing - await asyncio.sleep(random.uniform(0.2, 0.6)) - - logger.debug(f"✅ Human-typed '{text}' into {selector}") - - except Exception as e: - logger.warning(f"⚠️ Human typing failed: {e}") - - async def _human_click(self, selector: str, wait_before: bool = True, wait_after: bool = True, fast_mode: bool = True): - """ - Perform a human-like click with realistic delays and mouse movement. - - Args: - selector: CSS selector or element to click - wait_before: Whether to wait before clicking - wait_after: Whether to wait after clicking - fast_mode: If True, use minimal delays for speed optimization - """ - if not self.page: - logger.warning("Cannot perform human click: page is None") - return - - try: - if fast_mode: - # FAST MODE: Direct click with minimal delay - if wait_before: - await asyncio.sleep(random.uniform(0.02, 0.08)) # Reduced from 0.05-0.15 - - await self.page.click(selector) - - if wait_after: - await asyncio.sleep(random.uniform(0.02, 0.08)) # Reduced from 0.05-0.15 - - else: - # FULL MODE: Realistic mouse movement and timing - # Wait before clicking (thinking time) - if wait_before: - await asyncio.sleep(random.uniform(0.3, 0.8)) - - # Get element bounds for realistic mouse movement - element = await self.page.query_selector(selector) - if element: - box = await element.bounding_box() - if box: - # Move to element with slight randomness - center_x = box['x'] + box['width'] / 2 - center_y = box['y'] + box['height'] / 2 - - # Add small random offset - offset_x = random.uniform(-10, 10) - offset_y = random.uniform(-5, 5) - - await self.page.mouse.move(center_x + offset_x, center_y + offset_y) - await asyncio.sleep(random.uniform(0.1, 0.3)) - - # Perform click - await self.page.mouse.click(center_x + offset_x, center_y + offset_y) - - logger.debug(f"✅ Human-clicked {selector}") - else: - # Fallback to regular click - await self.page.click(selector) - logger.debug(f"✅ Fallback-clicked {selector}") - else: - logger.warning(f"⚠️ Element not found for human click: {selector}") - return - - # Wait after clicking (processing time) - if wait_after: - await asyncio.sleep(random.uniform(0.2, 0.6)) - - logger.debug(f"✅ Human-clicked {selector}") - - except Exception as e: - logger.warning(f"⚠️ Human click failed: {e}") - - async def _simulate_page_exploration(self, fast_mode: bool = True): - """ - Simulate natural page exploration before performing the main task. - This helps establish a more human-like session. - - Args: - fast_mode: If True, use minimal exploration for speed optimization - """ - if not self.page: - return - - logger.debug("🕵️ Simulating page exploration...") - - try: - if fast_mode: - # ULTRA-FAST MODE: Minimal exploration - await asyncio.sleep(random.uniform(0.05, 0.1)) # Reduced from 0.1-0.3 - - # Single mouse movement (optional) - try: - elements = await self.page.query_selector_all("input, button") - if elements and random.random() < 0.5: # 50% chance to skip - element = random.choice(elements) - box = await element.bounding_box() - if box: - center_x = box['x'] + box['width'] / 2 - center_y = box['y'] + box['height'] / 2 - await self.page.mouse.move(center_x, center_y) - except: - pass - - await asyncio.sleep(random.uniform(0.02, 0.05)) # Reduced from 0.05-0.15 - - else: - # FULL MODE: Comprehensive exploration - # 1. Brief pause to "read" the page - await asyncio.sleep(random.uniform(1.0, 2.5)) - - # 2. Move mouse to various UI elements (like a human would explore) - explore_selectors = [ - "h1", "h2", ".navbar", "#header", ".logo", - "input", "button", "a", ".form-group" - ] - - explored = 0 - for selector in explore_selectors: - elements = await self.page.query_selector_all(selector) - if elements and explored < 3: # Explore max 3 elements - element = random.choice(elements) - box = await element.bounding_box() - if box: - center_x = box['x'] + box['width'] / 2 - center_y = box['y'] + box['height'] / 2 - - await self.page.mouse.move(center_x, center_y) - await asyncio.sleep(random.uniform(0.3, 0.8)) - explored += 1 - - # 3. Small scroll to simulate reading - await self.page.mouse.wheel(0, random.randint(50, 150)) - await asyncio.sleep(random.uniform(0.5, 1.2)) - - logger.debug("✅ Page exploration completed") - - except Exception as e: - logger.debug(f"⚠️ Page exploration failed: {e}") - - 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}) - - logger.debug(f"Looking for table with ID: {self.RESULTS_TABLE_ID}") - if not table: - logger.warning(f"Table with ID '{self.RESULTS_TABLE_ID}' not found in HTML") - # Log available tables for debugging - all_tables = soup.find_all("table") - logger.debug(f"Found {len(all_tables)} tables in HTML") - for idx, tbl in enumerate(all_tables): - table_id = tbl.get('id', 'no-id') - table_class = tbl.get('class', 'no-class') - rows = tbl.find_all('tr') - logger.debug(f"Table {idx}: id='{table_id}', class='{table_class}', rows={len(rows)}") - - # If this looks like a results table, try to use it - if (table_id and ('grd' in table_id.lower() or 'kurul' in table_id.lower() or 'sonuc' in table_id.lower())) or \ - (isinstance(table_class, list) and any('grid' in cls.lower() or 'result' in cls.lower() for cls in table_class)) or \ - len(rows) > 3: # Table with multiple rows might be results - logger.info(f"Trying to parse table {idx} as potential results table: id='{table_id}'") - table = tbl - break - - if not table: - logger.error("No suitable results table found") - return entries - - rows = table.find_all("tr") - logger.info(f"Found {len(rows)} rows in results table") - - # Debug: Log first few rows structure - for i, row in enumerate(rows[:3]): - cells = row.find_all(["td", "th"]) - cell_texts = [cell.get_text(strip=True)[:30] for cell in cells] - logger.info(f"Row {i} structure: {len(cells)} cells: {cell_texts}") - - for row_idx, row in enumerate(rows): - # Skip first row (search bar with colspan=7) and second row (header with 6 cells) - if row_idx < 2: - logger.debug(f"Skipping header row {row_idx}") - continue - - cells = row.find_all("td") - logger.debug(f"Row {row_idx}: Found {len(cells)} cells") - - # Log cell contents for debugging - if cells and row_idx < 5: # Log first few data rows - for cell_idx, cell in enumerate(cells): - cell_text = cell.get_text(strip=True)[:50] # First 50 chars - logger.debug(f" Cell {cell_idx}: '{cell_text}...'") - - # Be more flexible with cell count - try 6 cells first, then adapt - if len(cells) >= 5: # At least 5 cells for minimum required data - try: - # Try to find preview button in first cell or any cell with a link - preview_button_tag = None - event_target = "" - - # Look for preview button in first few cells - for cell_idx in range(min(3, len(cells))): - cell = cells[cell_idx] - # Try multiple patterns for preview button (based on actual HTML structure) - preview_candidates = [ - cell.find("a", id="btnOnizle"), # Exact match - cell.find("a", id=re.compile(r"btnOnizle$")), - cell.find("a", id=re.compile(r"btn.*Onizle")), - cell.find("a", id=re.compile(r".*Onizle.*")), - cell.find("a", href=re.compile(r"__doPostBack")) - ] - - for candidate in preview_candidates: - if candidate and candidate.has_attr('href'): - match = re.search(r"__doPostBack\('([^']*)','([^']*)'\)", candidate['href']) - if match: - event_target = match.group(1) - preview_button_tag = candidate - logger.debug(f"Row {row_idx}: Found event_target '{event_target}' in cell {cell_idx}") - break - - if preview_button_tag: - break - - if not preview_button_tag: - logger.debug(f"Row {row_idx}: No preview button found in any cell") - # Log what links we found - for cell_idx, cell in enumerate(cells[:3]): - links_in_cell = cell.find_all("a") - logger.debug(f" Cell {cell_idx}: {len(links_in_cell)} links") - for link in links_in_cell[:2]: - logger.debug(f" Link id='{link.get('id')}', href='{link.get('href', '')[:50]}...'") - - # Try to find decision data spans with more flexible patterns - karar_no_span = None - karar_tarihi_span = None - idare_span = None - basvuru_sahibi_span = None - ihale_span = None - - # Try different span patterns for karar no (usually in cell 1) - for cell_idx in range(min(4, len(cells))): - if not karar_no_span: - cell = cells[cell_idx] - candidates = [ - cell.find("span", id="lblKno"), # Exact match based on actual HTML - cell.find("span", id=re.compile(r"lblKno$")), - cell.find("span", id=re.compile(r".*Kno.*")), - cell.find("span", id=re.compile(r".*KararNo.*")), - cell.find("span", id=re.compile(r".*No.*")) - ] - for candidate in candidates: - if candidate and candidate.get_text(strip=True): - karar_no_span = candidate - logger.debug(f"Row {row_idx}: Found karar_no in cell {cell_idx}") - break - - # Try different patterns for karar tarihi (usually in cell 2) - for cell_idx in range(min(4, len(cells))): - if not karar_tarihi_span: - cell = cells[cell_idx] - candidates = [ - cell.find("span", id="lblKtar"), # Exact match based on actual HTML - cell.find("span", id=re.compile(r"lblKtar$")), - cell.find("span", id=re.compile(r".*Ktar.*")), - cell.find("span", id=re.compile(r".*Tarih.*")), - cell.find("span", id=re.compile(r".*Date.*")) - ] - for candidate in candidates: - if candidate and candidate.get_text(strip=True): - # Check if it looks like a date - text = candidate.get_text(strip=True) - if re.match(r'\d{1,2}[./]\d{1,2}[./]\d{4}', text): - karar_tarihi_span = candidate - logger.debug(f"Row {row_idx}: Found karar_tarihi in cell {cell_idx}") - break - - # Find other spans in remaining cells (if we have 6 cells) - using exact IDs - if len(cells) >= 6: - idare_span = cells[3].find("span", id="lblIdare") or cells[3].find("span") - basvuru_sahibi_span = cells[4].find("span", id="lblSikayetci") or cells[4].find("span") - ihale_span = cells[5].find("span", id="lblIhale") or cells[5].find("span") - elif len(cells) == 5: - # Adjust for 5-cell layout - idare_span = cells[2].find("span") if cells[2] != cells[1] else None - basvuru_sahibi_span = cells[3].find("span") if len(cells) > 3 else None - ihale_span = cells[4].find("span") if len(cells) > 4 else None - - # Log what we found - logger.debug(f"Row {row_idx}: karar_no_span={karar_no_span is not None}, " - f"karar_tarihi_span={karar_tarihi_span is not None}, " - f"event_target={bool(event_target)}") - - # For KIK, we need at least karar_no and karar_tarihi, event_target is helpful but not critical - if not (karar_no_span and karar_tarihi_span): - logger.debug(f"Row {row_idx}: Missing required fields (karar_no or karar_tarihi), skipping") - # Log what spans we found in cells - for i, cell in enumerate(cells): - spans = cell.find_all("span") - if spans: - span_info = [] - for s in spans: - span_id = s.get('id', 'no-id') - span_text = s.get_text(strip=True)[:20] - span_info.append(f"{span_id}:'{span_text}...'") - logger.debug(f" Cell {i} spans: {span_info}") - continue - - # If we don't have event_target, we can still create an entry but mark it specially - if not event_target: - logger.warning(f"Row {row_idx}: No event_target found, document retrieval won't work") - event_target = f"missing_target_row_{row_idx}" # Placeholder - - # Karar tipini arama parametresinden alıyoruz, çünkü HTML'de direkt olarak bulunmuyor. - try: - 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) - logger.info(f"Row {row_idx}: Successfully parsed decision: {entry.karar_no_str}") - except Exception as e: - logger.error(f"Row {row_idx}: Error creating KikDecisionEntry: {e}") - continue - - except Exception as e: - logger.error(f"Error parsing row {row_idx}: {e}", exc_info=True) - else: - logger.warning(f"Row {row_idx}: Expected at least 5 cells but found {len(cells)}, skipping") - if len(cells) > 0: - cell_texts = [cell.get_text(strip=True)[:50] for cell in cells[:3]] - logger.debug(f"Row {row_idx} cells preview: {cell_texts}") - - logger.info(f"Parsed {len(entries)} decision entries from {len(rows)} rows") - 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) - - # Simulate natural page exploration after navigation (FAST MODE) - await self._simulate_page_exploration(fast_mode=True) - - 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}" - logger.info(f"Selecting radio button: {js_target_radio}") - async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout): - await page.evaluate(f"javascript:__doPostBack('{js_target_radio}','')") - # Ultra-fast wait for page to stabilize after radio button change - await page.wait_for_timeout(300) # Reduced from 1000ms - logger.info("Radio button selection completed") - - # Helper function for human-like form filling (FAST MODE) - async def human_fill_if_value(selector_key: str, value: Optional[str]): - if value is not None: - selector = self.FIELD_LOCATORS[selector_key] - await self._human_type(selector, value, fast_mode=True) - - # 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.") - - # Fill form fields with FAST human-like behavior - logger.info("Filling form fields with fast mode...") - - # Start with FAST mouse behavior simulation - await self._simulate_human_behavior(fast_mode=True) - - await human_fill_if_value('karar_metni', search_params.karar_metni) - await human_fill_if_value('karar_no', karar_no_for_kik_form) # Dönüştürülmüş halini kullan - await human_fill_if_value('karar_tarihi_baslangic', search_params.karar_tarihi_baslangic) - await human_fill_if_value('karar_tarihi_bitis', search_params.karar_tarihi_bitis) - await human_fill_if_value('resmi_gazete_sayisi', search_params.resmi_gazete_sayisi) - await human_fill_if_value('resmi_gazete_tarihi', search_params.resmi_gazete_tarihi) - await human_fill_if_value('basvuru_konusu_ihale', search_params.basvuru_konusu_ihale) - await human_fill_if_value('basvuru_sahibi', search_params.basvuru_sahibi) - await human_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) - await page.wait_for_timeout(50) # Reduced from 100ms - - logger.info("Form filling completed, preparing for search...") - - # Additional FAST human behavior before search - await self._simulate_human_behavior(fast_mode=True) - - action_is_search_button_click = (search_params.page == 1) - event_target_for_submit: str - - try: - if action_is_search_button_click: - event_target_for_submit = self.FIELD_LOCATORS['search_button_id'] - # Use human-like clicking for search button - search_button_selector = f"a[id='{event_target_for_submit}']" - logger.info(f"Performing human-like search button click...") - - try: - # Hide datepicker first to prevent interference - await page.evaluate("$('#ui-datepicker-div').hide()") - - # FAST Human-like click on search button - await self._human_click(search_button_selector, wait_before=True, wait_after=False, fast_mode=True) - - # Wait for navigation - await page.wait_for_load_state("networkidle", timeout=self.request_timeout) - logger.info("Search navigation completed successfully") - except Exception as e: - logger.warning(f"Human click failed, falling back to JavaScript: {e}") - # Hide datepicker and use JavaScript fallback - await page.evaluate("$('#ui-datepicker-div').hide()") - async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout): - await page.evaluate(f"javascript:__doPostBack('{event_target_for_submit}','')") - logger.info("Search navigation completed via fallback") - else: - # Pagination - use original method for consistency - page_link_ctl_number = search_params.page + 2 - event_target_for_submit = f"ctl00$ContentPlaceHolder1$grdKurulKararSorguSonuc$ctl14$ctl{page_link_ctl_number:02d}" - logger.info(f"Executing pagination with event target: {event_target_for_submit}") - - async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout): - await page.evaluate(f"javascript:__doPostBack('{event_target_for_submit}','')") - logger.info("Pagination navigation completed successfully") - except PlaywrightTimeoutError: - logger.warning("Search navigation timed out, but continuing...") - await page.wait_for_timeout(5000) # Longer wait if navigation fails - - # Ultra-fast wait time for results to load - logger.info("Waiting for search results to load...") - await page.wait_for_timeout(500) # Reduced from 1000ms - - results_table_dom_selector = f"table#{self.RESULTS_TABLE_ID}" - try: - # First wait for any tables to appear (more general check) - logger.info("Waiting for any tables to appear...") - await page.wait_for_function(""" - () => document.querySelectorAll('table').length > 0 - """, timeout=4000) # Reduced from 8000ms - logger.info("At least one table appeared") - - # Then wait for our specific table - await page.wait_for_selector(results_table_dom_selector, timeout=4000, state="attached") # Reduced from 8000ms - logger.debug("Results table attached to DOM") - - # Wait for table to have some content (more than just headers) - await page.wait_for_function(f""" - () => {{ - const table = document.querySelector('{results_table_dom_selector}'); - return table && table.querySelectorAll('tr').length > 2; - }} - """, timeout=4000) # Reduced from 20000ms - logger.debug("Results table populated with data") - - # Ultra-fast additional wait for any remaining JavaScript - await page.wait_for_timeout(500) # Reduced from 3000ms - - except PlaywrightTimeoutError: - logger.warning(f"Timeout waiting for results table '{results_table_dom_selector}'.") - # Try one more wait for content placeholder - try: - await page.wait_for_selector("#ctl00_ContentPlaceHolder1", timeout=10000) - logger.info("ContentPlaceHolder1 found, checking for tables...") - await page.wait_for_timeout(5000) - except PlaywrightTimeoutError: - logger.warning("ContentPlaceHolder1 also not found - content may not have loaded") - - 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 - try: - # Convert HTML string to bytes and create BytesIO stream - html_bytes = cleaned_html.encode('utf-8') - html_stream = io.BytesIO(html_bytes) - - # Pass BytesIO stream to MarkItDown to avoid temp file creation - md_converter = MarkItDown(enable_plugins=True, remove_alt_whitespace=True, keep_underline=True) - markdown_output = md_converter.convert(html_stream).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) - 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) diff --git a/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/models.py deleted file mode 100644 index 6b6632b..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/kik_mcp_module/models.py +++ /dev/null @@ -1,74 +0,0 @@ -# kik_mcp_module/models.py -from pydantic import BaseModel, Field, HttpUrl, computed_field, ConfigDict -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") - karar_no: str = Field("", description="No") - karar_tarihi_baslangic: str = Field("", description="Start", pattern=r"^\d{2}\.\d{2}\.\d{4}$|^$") - karar_tarihi_bitis: str = Field("", description="End", pattern=r"^\d{2}\.\d{2}\.\d{4}$|^$") - resmi_gazete_sayisi: str = Field("", description="Gazette") - resmi_gazete_tarihi: str = Field("", description="Date", pattern=r"^\d{2}\.\d{2}\.\d{4}$|^$") - basvuru_konusu_ihale: str = Field("", description="Subject") - basvuru_sahibi: str = Field("", description="Applicant") - ihaleyi_yapan_idare: str = Field("", description="Entity") - yil: str = Field("", description="Year") - karar_metni: str = Field("", description="Text") - page: int = Field(1, ge=1, description="Page") - -class KikDecisionEntry(BaseModel): - """Represents a single decision entry from KIK search results.""" - preview_event_target: str = Field(..., description="Event target") - karar_no_str: str = Field(..., alias="kararNo", description="Decision number") - karar_tipi: KikKararTipi = Field(..., description="Decision type") - - karar_tarihi_str: str = Field(..., alias="kararTarihi", description="Date") - idare_str: str = Field("", alias="idare", description="Entity") - basvuru_sahibi_str: str = Field("", alias="basvuruSahibi", description="Applicant") - ihale_konusu_str: str = Field("", alias="ihaleKonusu", description="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') - - model_config = ConfigDict(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="Request ID") - retrieved_karar_no: Optional[str] = Field(None, description="Decision number") - retrieved_karar_tipi: Optional[KikKararTipi] = Field(None, description="Decision type") - - karar_id_param_from_url: Optional[str] = Field(None, alias="kararIdParam", description="Internal ID") - markdown_chunk: Optional[str] = Field(None, description="Content") - source_url: Optional[str] = Field(None, description="Source URL") - error_message: Optional[str] = Field(None, description="Error") - current_page: int = Field(1, description="Page") - total_pages: int = Field(1, description="Total pages") - is_paginated: bool = Field(False, description="Paginated") - full_content_char_count: Optional[int] = Field(None, description="Char count") - - model_config = ConfigDict(populate_by_name=True) \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/__init__.py deleted file mode 100644 index 0409b6b..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# kvkk_mcp_module/__init__.py \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/client.py deleted file mode 100644 index ab26bb8..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/client.py +++ /dev/null @@ -1,372 +0,0 @@ -# kvkk_mcp_module/client.py - -import httpx -from bs4 import BeautifulSoup -from typing import List, Optional, Dict, Any -import logging -import os -import re -import io -import math -from urllib.parse import urljoin, urlparse, parse_qs -from markitdown import MarkItDown -from pydantic import HttpUrl - -from .models import ( - KvkkSearchRequest, - KvkkDecisionSummary, - KvkkSearchResult, - KvkkDocumentMarkdown -) - -logger = logging.getLogger(__name__) -if not logger.hasHandlers(): - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' - ) - -class KvkkApiClient: - """ - API client for searching and retrieving KVKK (Personal Data Protection Authority) decisions - using Brave Search API for discovery and direct HTTP requests for content retrieval. - """ - - BRAVE_API_URL = "https://api.search.brave.com/res/v1/web/search" - KVKK_BASE_URL = "https://www.kvkk.gov.tr" - DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000 # Character limit per page - - def __init__(self, request_timeout: float = 60.0): - """Initialize the KVKK API client.""" - self.brave_api_token = os.getenv("BRAVE_API_TOKEN") - if not self.brave_api_token: - # Fallback to provided free token - self.brave_api_token = "BSAuaRKB-dvSDSQxIN0ft1p2k6N82Kq" - logger.info("Using fallback Brave API token (limited free token)") - else: - logger.info("Using Brave API token from environment variable") - - self.http_client = httpx.AsyncClient( - headers={ - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - }, - timeout=request_timeout, - verify=True, - follow_redirects=True - ) - - def _construct_search_query(self, keywords: str) -> str: - """Construct the search query for Brave API.""" - base_query = 'site:kvkk.gov.tr "karar özeti"' - if keywords.strip(): - return f"{base_query} {keywords.strip()}" - return base_query - - def _extract_decision_id_from_url(self, url: str) -> Optional[str]: - """Extract decision ID from KVKK decision URL.""" - try: - # Example URL: https://www.kvkk.gov.tr/Icerik/7288/2021-1303 - parsed_url = urlparse(url) - path_parts = parsed_url.path.strip('/').split('/') - - if len(path_parts) >= 3 and path_parts[0] == 'Icerik': - # Extract the decision ID from the path - decision_id = '/'.join(path_parts[1:]) # e.g., "7288/2021-1303" - return decision_id - - except Exception as e: - logger.debug(f"Could not extract decision ID from URL {url}: {e}") - - return None - - def _extract_decision_metadata_from_title(self, title: str) -> Dict[str, Optional[str]]: - """Extract decision metadata from title string.""" - metadata = { - "decision_date": None, - "decision_number": None - } - - if not title: - return metadata - - # Extract decision date (DD/MM/YYYY format) - date_match = re.search(r'(\d{1,2}/\d{1,2}/\d{4})', title) - if date_match: - metadata["decision_date"] = date_match.group(1) - - # Extract decision number (YYYY/XXXX format) - number_match = re.search(r'(\d{4}/\d+)', title) - if number_match: - metadata["decision_number"] = number_match.group(1) - - return metadata - - async def search_decisions(self, params: KvkkSearchRequest) -> KvkkSearchResult: - """Search for KVKK decisions using Brave API.""" - - search_query = self._construct_search_query(params.keywords) - logger.info(f"KvkkApiClient: Searching with query: {search_query}") - - try: - # Calculate offset for pagination - offset = (params.page - 1) * params.pageSize - - response = await self.http_client.get( - self.BRAVE_API_URL, - headers={ - "Accept": "application/json", - "Accept-Encoding": "gzip", - "x-subscription-token": self.brave_api_token - }, - params={ - "q": search_query, - "country": "TR", - "search_lang": "tr", - "ui_lang": "tr-TR", - "offset": offset, - "count": params.pageSize - } - ) - - response.raise_for_status() - data = response.json() - - # Extract search results - decisions = [] - web_results = data.get("web", {}).get("results", []) - - for result in web_results: - title = result.get("title", "") - url = result.get("url", "") - description = result.get("description", "") - - # Extract metadata from title - metadata = self._extract_decision_metadata_from_title(title) - - # Extract decision ID from URL - decision_id = self._extract_decision_id_from_url(url) - - decision = KvkkDecisionSummary( - title=title, - url=HttpUrl(url) if url else None, - description=description, - decision_id=decision_id, - publication_date=metadata.get("decision_date"), - decision_number=metadata.get("decision_number") - ) - decisions.append(decision) - - # Get total results if available - total_results = None - query_info = data.get("query", {}) - if "total_results" in query_info: - total_results = query_info["total_results"] - - return KvkkSearchResult( - decisions=decisions, - total_results=total_results, - page=params.page, - pageSize=params.pageSize, - query=search_query - ) - - except httpx.RequestError as e: - logger.error(f"KvkkApiClient: HTTP request error during search: {e}") - return KvkkSearchResult( - decisions=[], - total_results=0, - page=params.page, - pageSize=params.pageSize, - query=search_query - ) - except Exception as e: - logger.error(f"KvkkApiClient: Unexpected error during search: {e}") - return KvkkSearchResult( - decisions=[], - total_results=0, - page=params.page, - pageSize=params.pageSize, - query=search_query - ) - - def _extract_decision_content_from_html(self, html: str, url: str) -> Dict[str, Any]: - """Extract decision content from KVKK decision page HTML.""" - try: - soup = BeautifulSoup(html, 'html.parser') - - # Extract title - title = None - title_element = soup.find('h3', class_='blog-post-title') - if title_element: - title = title_element.get_text(strip=True) - elif soup.title: - title = soup.title.get_text(strip=True) - - # Extract decision content from the main content div - content_div = soup.find('div', class_='blog-post-inner') - if not content_div: - # Fallback to other possible content containers - content_div = soup.find('div', style='text-align:justify;') - if not content_div: - logger.warning(f"Could not find decision content div in {url}") - return { - "title": title, - "decision_date": None, - "decision_number": None, - "subject_summary": None, - "html_content": None - } - - # Extract decision metadata from table - decision_date = None - decision_number = None - subject_summary = None - - table = content_div.find('table') - if table: - rows = table.find_all('tr') - for row in rows: - cells = row.find_all('td') - if len(cells) >= 3: - field_name = cells[0].get_text(strip=True) - field_value = cells[2].get_text(strip=True) - - if 'Karar Tarihi' in field_name: - decision_date = field_value - elif 'Karar No' in field_name: - decision_number = field_value - elif 'Konu Özeti' in field_name: - subject_summary = field_value - - return { - "title": title, - "decision_date": decision_date, - "decision_number": decision_number, - "subject_summary": subject_summary, - "html_content": str(content_div) - } - - except Exception as e: - logger.error(f"Error extracting content from HTML for {url}: {e}") - return { - "title": None, - "decision_date": None, - "decision_number": None, - "subject_summary": None, - "html_content": None - } - - def _convert_html_to_markdown(self, html_content: str) -> Optional[str]: - """Convert HTML content to Markdown using MarkItDown with BytesIO to avoid filename length issues.""" - if not html_content: - return None - - try: - # Convert HTML string to bytes and create BytesIO stream - html_bytes = html_content.encode('utf-8') - html_stream = io.BytesIO(html_bytes) - - # Pass BytesIO stream to MarkItDown to avoid temp file creation - md_converter = MarkItDown(enable_plugins=False) - result = md_converter.convert(html_stream) - return result.text_content - except Exception as e: - logger.error(f"Error converting HTML to Markdown: {e}") - return None - - async def get_decision_document(self, decision_url: str, page_number: int = 1) -> KvkkDocumentMarkdown: - """Retrieve and convert a KVKK decision document to paginated Markdown.""" - logger.info(f"KvkkApiClient: Getting decision document from: {decision_url}, page: {page_number}") - - try: - # Fetch the decision page - response = await self.http_client.get(decision_url) - response.raise_for_status() - - # Extract content from HTML - extracted_data = self._extract_decision_content_from_html(response.text, decision_url) - - # Convert HTML content to Markdown - full_markdown_content = None - if extracted_data["html_content"]: - full_markdown_content = self._convert_html_to_markdown(extracted_data["html_content"]) - - if not full_markdown_content: - return KvkkDocumentMarkdown( - source_url=HttpUrl(decision_url), - title=extracted_data["title"], - decision_date=extracted_data["decision_date"], - decision_number=extracted_data["decision_number"], - subject_summary=extracted_data["subject_summary"], - markdown_chunk=None, - current_page=page_number, - total_pages=0, - is_paginated=False, - error_message="Could not convert document content to Markdown" - ) - - # Calculate pagination - content_length = len(full_markdown_content) - total_pages = math.ceil(content_length / self.DOCUMENT_MARKDOWN_CHUNK_SIZE) - if total_pages == 0: - total_pages = 1 - - # Clamp page number to valid range - current_page_clamped = max(1, min(page_number, total_pages)) - - # Extract the requested chunk - start_index = (current_page_clamped - 1) * self.DOCUMENT_MARKDOWN_CHUNK_SIZE - end_index = start_index + self.DOCUMENT_MARKDOWN_CHUNK_SIZE - markdown_chunk = full_markdown_content[start_index:end_index] - - return KvkkDocumentMarkdown( - source_url=HttpUrl(decision_url), - title=extracted_data["title"], - decision_date=extracted_data["decision_date"], - decision_number=extracted_data["decision_number"], - subject_summary=extracted_data["subject_summary"], - markdown_chunk=markdown_chunk, - current_page=current_page_clamped, - total_pages=total_pages, - is_paginated=(total_pages > 1), - error_message=None - ) - - except httpx.HTTPStatusError as e: - error_msg = f"HTTP error {e.response.status_code} when fetching decision document" - logger.error(f"KvkkApiClient: {error_msg}") - return KvkkDocumentMarkdown( - source_url=HttpUrl(decision_url), - title=None, - decision_date=None, - decision_number=None, - subject_summary=None, - markdown_chunk=None, - current_page=page_number, - total_pages=0, - is_paginated=False, - error_message=error_msg - ) - except Exception as e: - error_msg = f"Unexpected error when fetching decision document: {str(e)}" - logger.error(f"KvkkApiClient: {error_msg}") - return KvkkDocumentMarkdown( - source_url=HttpUrl(decision_url), - title=None, - decision_date=None, - decision_number=None, - subject_summary=None, - markdown_chunk=None, - current_page=page_number, - total_pages=0, - is_paginated=False, - error_message=error_msg - ) - - async def close_client_session(self): - """Close the HTTP client session.""" - if hasattr(self, 'http_client') and self.http_client and not self.http_client.is_closed: - await self.http_client.aclose() - logger.info("KvkkApiClient: HTTP client session closed.") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/models.py b/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/models.py deleted file mode 100644 index 7db1c3c..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/kvkk_mcp_module/models.py +++ /dev/null @@ -1,49 +0,0 @@ -# kvkk_mcp_module/models.py - -from pydantic import BaseModel, Field, HttpUrl -from typing import List, Optional, Any - -class KvkkSearchRequest(BaseModel): - """Model for KVKK (Personal Data Protection Authority) search request via Brave API.""" - keywords: str = Field(..., description=""" - Keywords to search for in KVKK decisions. - The search will automatically include 'site:kvkk.gov.tr "karar özeti"' to target KVKK decision summaries. - Examples: "açık rıza", "veri güvenliği", "kişisel veri işleme" - """) - page: int = Field(1, ge=1, le=50, description="Page number for search results (1-50).") - pageSize: int = Field(10, ge=1, le=10, description="Number of results per page (1-10).") - -class KvkkDecisionSummary(BaseModel): - """Model for a single KVKK decision summary from Brave search results.""" - title: Optional[str] = Field(None, description="Decision title from search results.") - url: Optional[HttpUrl] = Field(None, description="URL to the KVKK decision page.") - description: Optional[str] = Field(None, description="Brief description or snippet from search results.") - decision_id: Optional[str] = Field(None, description="Value") - publication_date: Optional[str] = Field(None, description="Value") - decision_number: Optional[str] = Field(None, description="Value") - -class KvkkSearchResult(BaseModel): - """Model for the overall search result for KVKK decisions.""" - decisions: List[KvkkDecisionSummary] = Field(default_factory=list, description="List of KVKK decisions found.") - total_results: Optional[int] = Field(None, description="Value") - page: int = Field(1, description="Current page number of results.") - pageSize: int = Field(10, description="Number of results per page.") - query: Optional[str] = Field(None, description="The actual search query sent to Brave API.") - -class KvkkDocumentMarkdown(BaseModel): - """Model for KVKK decision document content converted to paginated Markdown.""" - source_url: HttpUrl = Field(description="URL of the original KVKK decision page.") - title: Optional[str] = Field(None, description="Title of the KVKK decision.") - decision_date: Optional[str] = Field(None, description="Decision date (Karar Tarihi).") - decision_number: Optional[str] = Field(None, description="Decision number (Karar No).") - subject_summary: Optional[str] = Field(None, description="Subject summary (Konu Özeti).") - markdown_chunk: Optional[str] = Field(None, description="A 5,000 character chunk of the Markdown content.") - current_page: int = Field(description="The current page number of the markdown chunk (1-indexed).") - total_pages: int = Field(description="Total number of pages for the full markdown content.") - is_paginated: bool = Field(description="True if the full markdown content is split into multiple pages.") - error_message: Optional[str] = Field(None, description="Value") - - class Config: - json_encoders = { - HttpUrl: str - } \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/__init__.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/__init__.py deleted file mode 100644 index 0ed22c9..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -MCP Auth Toolkit - OAuth 2.1 + Authorization for Model Context Protocol Servers -Integrated with Clerk Authentication -""" - -from .middleware import ( - AuthContext, - FastMCPAuthWrapper, - MCPAuthMiddleware, - auth_required, -) -from .oauth import OAuthConfig, OAuthProvider -from .policy import PolicyEngine, ToolPolicy, create_default_policies -from .storage import PersistentStorage - -__version__ = "0.1.0" -__all__ = [ - "OAuthProvider", - "OAuthConfig", - "AuthContext", - "auth_required", - "create_default_policies", - "MCPAuthMiddleware", - "FastMCPAuthWrapper", - "PolicyEngine", - "ToolPolicy", - "PersistentStorage", -] \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/clerk_config.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/clerk_config.py deleted file mode 100644 index 2c3e205..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/clerk_config.py +++ /dev/null @@ -1,73 +0,0 @@ -""" -Clerk OAuth configuration for MCP Auth Toolkit -""" - -import os -import logging -from .oauth import OAuthConfig - -logger = logging.getLogger(__name__) - - -def create_clerk_oauth_config() -> OAuthConfig: - """Create OAuth configuration for Clerk integration using SDK""" - - # Get Clerk configuration from environment - clerk_domain = os.getenv("CLERK_DOMAIN", "accounts.yargimcp.com") - clerk_publishable_key = os.getenv("CLERK_PUBLISHABLE_KEY") - clerk_secret_key = os.getenv("CLERK_SECRET_KEY") - - if not clerk_publishable_key or not clerk_secret_key: - raise ValueError("CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY are required") - - # For Clerk with custom domains, we use our adapter endpoints - # This allows us to handle the custom domain flow properly - base_url = os.getenv("BASE_URL", "https://yargimcp.com") - - config = OAuthConfig( - client_id=clerk_publishable_key, - client_secret=clerk_secret_key, - # Use our adapter endpoints instead of Clerk's direct endpoints - authorization_endpoint=f"{base_url}/authorize", - token_endpoint=f"{base_url}/token", - # Keep Clerk's JWKS for token validation - jwks_uri=f"https://{clerk_domain}/.well-known/jwks.json", - issuer=base_url, # We're the issuer for MCP tokens - scopes=["mcp:tools:read", "mcp:tools:write", "openid", "profile", "email"] - ) - - logger.info(f"Created Clerk OAuth config with adapter endpoints") - logger.info(f"Clerk domain: {clerk_domain}") - logger.debug(f"Authorization endpoint: {config.authorization_endpoint}") - logger.debug(f"Token endpoint: {config.token_endpoint}") - - return config - - -def get_jwt_secret() -> str: - """Get JWT secret for token signing""" - jwt_secret = os.getenv("JWT_SECRET_KEY") - - if not jwt_secret: - raise ValueError("JWT_SECRET_KEY environment variable is required") - - return jwt_secret - - -def create_mcp_server_config(): - """Create complete MCP server configuration for Clerk integration""" - - try: - oauth_config = create_clerk_oauth_config() - jwt_secret = get_jwt_secret() - - return { - "oauth_config": oauth_config, - "jwt_secret": jwt_secret, - "base_url": os.getenv("BASE_URL", "https://yargi-mcp.fly.dev"), - "auth_enabled": os.getenv("ENABLE_AUTH", "true").lower() == "true" - } - - except Exception as e: - logger.error(f"Failed to create MCP server config: {e}") - raise \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/middleware.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/middleware.py deleted file mode 100644 index 0dc6301..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/middleware.py +++ /dev/null @@ -1,315 +0,0 @@ -""" -MCP server middleware for OAuth authentication and authorization -""" - -import functools -import logging -from collections.abc import Callable -from dataclasses import dataclass -from typing import Any, Optional - -logger = logging.getLogger(__name__) - -try: - from fastmcp import FastMCP - FASTMCP_AVAILABLE = True -except ImportError: - FASTMCP_AVAILABLE = False - FastMCP = None - logger.warning("FastMCP not available, some features will be disabled") - -from .oauth import OAuthProvider -from .policy import PolicyEngine - - -@dataclass -class AuthContext: - """Authentication context passed to MCP tools""" - - user_id: str - scopes: list[str] - claims: dict[str, Any] - token: str - - -class MCPAuthMiddleware: - """Authentication middleware for MCP servers""" - - def __init__(self, oauth_provider: OAuthProvider, policy_engine: PolicyEngine): - self.oauth_provider = oauth_provider - self.policy_engine = policy_engine - - def authenticate_request(self, authorization_header: str) -> AuthContext | None: - """Extract and validate auth token from request""" - - if not authorization_header: - logger.debug("No authorization header provided") - return None - - if not authorization_header.startswith("Bearer "): - logger.debug("Authorization header does not start with 'Bearer '") - return None - - token = authorization_header[7:] # Remove 'Bearer ' prefix - - token_info = self.oauth_provider.introspect_token(token) - - if not token_info.get("active"): - logger.warning("Token is not active") - return None - - logger.debug(f"Authenticated user: {token_info.get('sub', 'unknown')}") - - return AuthContext( - user_id=token_info.get("sub", "unknown"), - scopes=token_info.get("mcp_tool_scopes", []), - claims=token_info, - token=token, - ) - - def authorize_tool_call( - self, tool_name: str, auth_context: AuthContext - ) -> tuple[bool, str | None]: - """Check if user can call the specified tool""" - - return self.policy_engine.authorize_tool_call( - tool_name=tool_name, - user_scopes=auth_context.scopes, - user_claims=auth_context.claims, - ) - - -def auth_required( - oauth_provider: OAuthProvider, - policy_engine: PolicyEngine, - tool_name: str | None = None, -): - """ - Decorator to require authentication for MCP tool functions - - Usage: - @auth_required(oauth_provider, policy_engine, "search_yargitay") - def my_tool_function(context: AuthContext, ...): - pass - """ - - def decorator(func: Callable) -> Callable: - middleware = MCPAuthMiddleware(oauth_provider, policy_engine) - - @functools.wraps(func) - async def wrapper(*args, **kwargs): - # Extract authorization header from kwargs - auth_header = kwargs.pop("authorization", None) - - # Also check in args if it's a Request object - if not auth_header and args: - for arg in args: - if hasattr(arg, 'headers'): - auth_header = arg.headers.get("Authorization") - break - - if not auth_header: - logger.warning(f"No authorization header for tool '{tool_name or func.__name__}'") - raise PermissionError("Authorization header required") - - auth_context = middleware.authenticate_request(auth_header) - - if not auth_context: - logger.warning(f"Authentication failed for tool '{tool_name or func.__name__}'") - raise PermissionError("Invalid or expired token") - - actual_tool_name = tool_name or func.__name__ - - authorized, reason = middleware.authorize_tool_call( - actual_tool_name, auth_context - ) - - if not authorized: - logger.warning(f"Authorization failed for tool '{actual_tool_name}': {reason}") - raise PermissionError(f"Access denied: {reason}") - - # Add auth context to function call - return await func(auth_context, *args, **kwargs) - - return wrapper - - return decorator - - -class FastMCPAuthWrapper: - """Wrapper for FastMCP servers to add authentication""" - - def __init__( - self, - mcp_server: "FastMCP", - oauth_provider: OAuthProvider, - policy_engine: PolicyEngine, - ): - if not FASTMCP_AVAILABLE: - raise ImportError("FastMCP is required for FastMCPAuthWrapper") - - self.mcp_server = mcp_server - self.middleware = MCPAuthMiddleware(oauth_provider, policy_engine) - self.oauth_provider = oauth_provider - logger.info("Initializing FastMCP authentication wrapper") - self._wrap_tools() - - def _wrap_tools(self): - """Wrap all existing tools with auth middleware""" - - # Try different FastMCP tool storage locations - tool_registry = None - - if hasattr(self.mcp_server, '_tools'): - tool_registry = self.mcp_server._tools - elif hasattr(self.mcp_server, 'tools'): - tool_registry = self.mcp_server.tools - elif hasattr(self.mcp_server, '_tool_registry'): - tool_registry = self.mcp_server._tool_registry - elif hasattr(self.mcp_server, '_handlers') and hasattr(self.mcp_server._handlers, 'tools'): - tool_registry = self.mcp_server._handlers.tools - - if not tool_registry: - logger.warning("FastMCP server tool registry not found, tools will not be automatically wrapped") - logger.debug(f"Available server attributes: {dir(self.mcp_server)}") - return - - logger.debug(f"Found tool registry with {len(tool_registry)} tools") - original_tools = dict(tool_registry) - wrapped_count = 0 - - for tool_name, tool_func in original_tools.items(): - try: - wrapped_func = self._create_auth_wrapper(tool_name, tool_func) - tool_registry[tool_name] = wrapped_func - wrapped_count += 1 - logger.debug(f"Wrapped tool: {tool_name}") - except Exception as e: - logger.error(f"Failed to wrap tool {tool_name}: {e}") - - logger.info(f"Successfully wrapped {wrapped_count} tools with authentication") - - def _create_auth_wrapper(self, tool_name: str, original_func: Callable) -> Callable: - """Create auth wrapper for a specific tool""" - - @functools.wraps(original_func) - async def auth_wrapper(*args, **kwargs): - # Extract authorization from various sources - auth_header = None - - # Check kwargs first - auth_header = kwargs.pop("authorization", None) - - # Check if first argument is a Request object - if not auth_header and args: - first_arg = args[0] - if hasattr(first_arg, 'headers'): - auth_header = first_arg.headers.get("Authorization") - - if not auth_header: - logger.warning(f"No authorization header for tool '{tool_name}'") - raise PermissionError("Authorization required") - - auth_context = self.middleware.authenticate_request(auth_header) - - if not auth_context: - logger.warning(f"Authentication failed for tool '{tool_name}'") - raise PermissionError("Invalid token") - - authorized, reason = self.middleware.authorize_tool_call( - tool_name, auth_context - ) - - if not authorized: - logger.warning(f"Authorization failed for tool '{tool_name}': {reason}") - raise PermissionError(f"Access denied: {reason}") - - # Add auth context to kwargs - kwargs["auth_context"] = auth_context - logger.debug(f"Calling tool '{tool_name}' for user {auth_context.user_id}") - - return await original_func(*args, **kwargs) - - return auth_wrapper - - def add_oauth_endpoints(self): - """Add OAuth endpoints to the MCP server""" - - @self.mcp_server.tool( - description="Initiate OAuth 2.1 authorization flow with PKCE", - annotations={"readOnlyHint": True, "idempotentHint": False} - ) - async def oauth_authorize(redirect_uri: str, scopes: Optional[str] = None): - """OAuth authorization endpoint""" - scope_list = scopes.split(" ") if scopes else None - auth_url, pkce = self.oauth_provider.generate_authorization_url( - redirect_uri=redirect_uri, scopes=scope_list - ) - logger.info(f"Generated authorization URL for redirect_uri: {redirect_uri}") - return { - "authorization_url": auth_url, - "code_verifier": pkce.verifier, # For PKCE flow - "code_challenge": pkce.challenge, - "instructions": "Use the authorization_url to complete OAuth flow, then exchange the returned code using oauth_token tool" - } - - @self.mcp_server.tool( - description="Exchange OAuth authorization code for access token", - annotations={"readOnlyHint": False, "idempotentHint": False} - ) - async def oauth_token( - code: str, - state: str, - redirect_uri: str - ): - """OAuth token exchange endpoint""" - try: - result = await self.oauth_provider.exchange_code_for_token( - code=code, state=state, redirect_uri=redirect_uri - ) - logger.info("Successfully exchanged authorization code for token") - return result - except Exception as e: - logger.error(f"Token exchange failed: {e}") - raise - - @self.mcp_server.tool( - description="Validate and introspect OAuth access token", - annotations={"readOnlyHint": True, "idempotentHint": True} - ) - async def oauth_introspect(token: str): - """Token introspection endpoint""" - result = self.oauth_provider.introspect_token(token) - logger.debug(f"Token introspection: active={result.get('active', False)}") - return result - - @self.mcp_server.tool( - description="Revoke OAuth access token", - annotations={"readOnlyHint": False, "idempotentHint": False} - ) - async def oauth_revoke(token: str): - """Token revocation endpoint""" - success = self.oauth_provider.revoke_token(token) - logger.info(f"Token revocation: success={success}") - return {"revoked": success} - - @self.mcp_server.tool( - description="Get list of tools available to authenticated user", - annotations={"readOnlyHint": True, "idempotentHint": True} - ) - async def oauth_user_tools(authorization: str): - """Get user's allowed tools based on scopes""" - auth_context = self.middleware.authenticate_request(authorization) - if not auth_context: - raise PermissionError("Invalid token") - - allowed_patterns = self.middleware.policy_engine.get_allowed_tools(auth_context.scopes) - - return { - "user_id": auth_context.user_id, - "scopes": auth_context.scopes, - "allowed_tool_patterns": allowed_patterns, - "message": "Use these patterns to determine which tools you can access" - } - - logger.info("Added OAuth endpoints: oauth_authorize, oauth_token, oauth_introspect, oauth_revoke, oauth_user_tools") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/oauth.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/oauth.py deleted file mode 100644 index efc3adc..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/oauth.py +++ /dev/null @@ -1,304 +0,0 @@ -""" -OAuth 2.1 + PKCE implementation for MCP servers with Clerk integration -""" - -import base64 -import hashlib -import secrets -import time -import logging -from dataclasses import dataclass -from datetime import datetime, timedelta -from typing import Any, Optional -from urllib.parse import urlencode - -import httpx -import jwt -from jwt.exceptions import PyJWTError, InvalidTokenError - -from .storage import PersistentStorage - -# Try to import Clerk SDK -try: - from clerk_backend_api import Clerk - CLERK_AVAILABLE = True -except ImportError: - CLERK_AVAILABLE = False - Clerk = None - -logger = logging.getLogger(__name__) - - -@dataclass -class OAuthConfig: - """OAuth provider configuration for Clerk""" - - client_id: str - client_secret: str - authorization_endpoint: str - token_endpoint: str - jwks_uri: str | None = None - issuer: str = "mcp-auth" - scopes: list[str] = None - - def __post_init__(self): - if self.scopes is None: - self.scopes = ["mcp:tools:read", "mcp:tools:write"] - - -class PKCEChallenge: - """PKCE challenge/verifier pair for OAuth 2.1""" - - def __init__(self): - self.verifier = ( - base64.urlsafe_b64encode(secrets.token_bytes(32)) - .decode("utf-8") - .rstrip("=") - ) - - challenge_bytes = hashlib.sha256(self.verifier.encode("utf-8")).digest() - self.challenge = ( - base64.urlsafe_b64encode(challenge_bytes).decode("utf-8").rstrip("=") - ) - - -class OAuthProvider: - """OAuth 2.1 provider with PKCE support and Clerk integration""" - - def __init__(self, config: OAuthConfig, jwt_secret: str): - self.config = config - self.jwt_secret = jwt_secret - # Use persistent storage instead of memory - self.storage = PersistentStorage() - - # Initialize Clerk SDK if available - self.clerk = None - if CLERK_AVAILABLE and config.client_secret: - try: - self.clerk = Clerk(bearer_auth=config.client_secret) - logger.info("Clerk SDK initialized successfully") - except Exception as e: - logger.warning(f"Failed to initialize Clerk SDK: {e}") - - logger.info("OAuth provider initialized with persistent storage") - - def generate_authorization_url( - self, - redirect_uri: str, - state: str | None = None, - scopes: list[str] | None = None, - ) -> tuple[str, PKCEChallenge]: - """Generate OAuth authorization URL with PKCE for Clerk""" - - pkce = PKCEChallenge() - session_id = secrets.token_urlsafe(32) - - if state is None: - state = secrets.token_urlsafe(16) - - if scopes is None: - scopes = self.config.scopes - - # Store session data with expiration - session_data = { - "pkce_verifier": pkce.verifier, - "state": state, - "redirect_uri": redirect_uri, - "scopes": scopes, - "created_at": time.time(), - "expires_at": (datetime.utcnow() + timedelta(minutes=10)).timestamp(), - } - self.storage.set_session(session_id, session_data) - - # Build Clerk OAuth URL - # Check if this is a custom domain (sign-in endpoint) - if self.config.authorization_endpoint.endswith('/sign-in'): - # For custom domains, Clerk expects redirect_url parameter - params = { - "redirect_url": redirect_uri, - "state": f"{state}:{session_id}", - } - auth_url = f"{self.config.authorization_endpoint}?{urlencode(params)}" - else: - # Standard OAuth flow with PKCE - params = { - "response_type": "code", - "client_id": self.config.client_id, - "redirect_uri": redirect_uri, - "scope": " ".join(scopes), - "state": f"{state}:{session_id}", # Combine state with session ID - "code_challenge": pkce.challenge, - "code_challenge_method": "S256", - } - auth_url = f"{self.config.authorization_endpoint}?{urlencode(params)}" - - logger.info(f"Generated OAuth URL with session {session_id[:8]}...") - logger.debug(f"Auth URL: {auth_url}") - return auth_url, pkce - - async def exchange_code_for_token( - self, code: str, state: str, redirect_uri: str - ) -> dict[str, Any]: - """Exchange authorization code for access token with Clerk""" - - try: - original_state, session_id = state.split(":", 1) - except ValueError as e: - logger.error(f"Invalid state format: {state}") - raise ValueError("Invalid state format") from e - - session = self.storage.get_session(session_id) - if not session: - logger.error(f"Session {session_id} not found") - raise ValueError("Invalid session") - - # Check session expiration - if datetime.utcnow().timestamp() > session.get("expires_at", 0): - self.storage.delete_session(session_id) - logger.error(f"Session {session_id} expired") - raise ValueError("Session expired") - - if session["state"] != original_state: - logger.error(f"State mismatch: expected {session['state']}, got {original_state}") - raise ValueError("State mismatch") - - if session["redirect_uri"] != redirect_uri: - logger.error(f"Redirect URI mismatch: expected {session['redirect_uri']}, got {redirect_uri}") - raise ValueError("Redirect URI mismatch") - - # Prepare token exchange request for Clerk - token_data = { - "grant_type": "authorization_code", - "client_id": self.config.client_id, - "client_secret": self.config.client_secret, - "code": code, - "redirect_uri": redirect_uri, - "code_verifier": session["pkce_verifier"], - } - - logger.info(f"Exchanging code with Clerk for session {session_id[:8]}...") - - async with httpx.AsyncClient() as client: - response = await client.post( - self.config.token_endpoint, - data=token_data, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - timeout=30.0, - ) - - if response.status_code != 200: - logger.error(f"Clerk token exchange failed: {response.status_code} - {response.text}") - raise ValueError(f"Token exchange failed: {response.text}") - - token_response = response.json() - logger.info("Successfully exchanged code for Clerk token") - - # Create MCP-scoped JWT token - access_token = self._create_mcp_token( - session["scopes"], token_response.get("access_token"), session_id - ) - - # Store token for introspection - token_id = secrets.token_urlsafe(16) - token_data = { - "access_token": access_token, - "scopes": session["scopes"], - "created_at": time.time(), - "expires_at": (datetime.utcnow() + timedelta(hours=1)).timestamp(), - "session_id": session_id, - "clerk_token": token_response.get("access_token"), - } - self.storage.set_token(token_id, token_data) - - # Clean up session - self.storage.delete_session(session_id) - - return { - "access_token": access_token, - "token_type": "bearer", - "expires_in": 3600, - "scope": " ".join(session["scopes"]), - } - - def validate_pkce(self, code_verifier: str, code_challenge: str) -> bool: - """Validate PKCE code challenge (RFC 7636)""" - # S256 method - verifier_hash = hashlib.sha256(code_verifier.encode()).digest() - expected_challenge = base64.urlsafe_b64encode(verifier_hash).decode().rstrip('=') - return expected_challenge == code_challenge - - def _create_mcp_token( - self, scopes: list[str], upstream_token: str, session_id: str - ) -> str: - """Create MCP-scoped JWT token with Clerk token embedded""" - - now = int(time.time()) - payload = { - "iss": self.config.issuer, - "sub": session_id, - "aud": "mcp-server", - "iat": now, - "exp": now + 3600, # 1 hour expiration - "mcp_tool_scopes": scopes, - "upstream_token": upstream_token, - "clerk_integration": True, - } - - return jwt.encode(payload, self.jwt_secret, algorithm="HS256") - - def introspect_token(self, token: str) -> dict[str, Any]: - """Introspect and validate MCP token""" - - try: - payload = jwt.decode(token, self.jwt_secret, algorithms=["HS256"]) - - # Check if token is expired - if payload.get("exp", 0) < time.time(): - return {"active": False, "error": "token_expired"} - - return { - "active": True, - "sub": payload.get("sub"), - "aud": payload.get("aud"), - "iss": payload.get("iss"), - "exp": payload.get("exp"), - "iat": payload.get("iat"), - "mcp_tool_scopes": payload.get("mcp_tool_scopes", []), - "upstream_token": payload.get("upstream_token"), - "clerk_integration": payload.get("clerk_integration", False), - } - - except PyJWTError as e: - logger.warning(f"Token validation failed: {e}") - return {"active": False, "error": "invalid_token"} - - def revoke_token(self, token: str) -> bool: - """Revoke a token""" - - try: - payload = jwt.decode(token, self.jwt_secret, algorithms=["HS256"]) - session_id = payload.get("sub") - - # Remove all tokens associated with this session - all_tokens = self.storage.get_tokens() - tokens_to_remove = [ - token_id - for token_id, token_data in all_tokens.items() - if token_data.get("session_id") == session_id - ] - - for token_id in tokens_to_remove: - self.storage.delete_token(token_id) - - logger.info(f"Revoked {len(tokens_to_remove)} tokens for session {session_id}") - return True - - except InvalidTokenError as e: - logger.warning(f"Token revocation failed: {e}") - return False - - def cleanup_expired_sessions(self): - """Clean up expired sessions and tokens""" - # This is now handled automatically by persistent storage - self.storage.cleanup_expired_sessions() - logger.debug("Cleanup completed via persistent storage") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/policy.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/policy.py deleted file mode 100644 index e947af3..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/policy.py +++ /dev/null @@ -1,201 +0,0 @@ -""" -Authorization policy engine for MCP tools -""" - -import re -import logging -from dataclasses import dataclass -from enum import Enum -from typing import Any - -logger = logging.getLogger(__name__) - - -class PolicyAction(Enum): - ALLOW = "allow" - DENY = "deny" - - -@dataclass -class ToolPolicy: - """Policy rule for MCP tool access""" - - tool_pattern: str # regex pattern for tool names - required_scopes: list[str] - action: PolicyAction = PolicyAction.ALLOW - conditions: dict[str, Any] | None = None - - def matches_tool(self, tool_name: str) -> bool: - """Check if the policy applies to given tool""" - return bool(re.match(self.tool_pattern, tool_name)) - - def evaluate_scopes(self, user_scopes: list[str]) -> bool: - """Check if user has required scopes""" - return all(scope in user_scopes for scope in self.required_scopes) - - -class PolicyEngine: - """Authorization policy engine for Turkish legal database tools""" - - def __init__(self): - self.policies: list[ToolPolicy] = [] - self.default_action = PolicyAction.DENY - - def add_policy(self, policy: ToolPolicy): - """Add a policy rule""" - self.policies.append(policy) - logger.debug(f"Added policy: {policy.tool_pattern} -> {policy.required_scopes}") - - def add_tool_scope_policy( - self, - tool_pattern: str, - required_scopes: str | list[str], - action: PolicyAction = PolicyAction.ALLOW, - ): - """Convenience method to add tool-scope policy""" - if isinstance(required_scopes, str): - required_scopes = [required_scopes] - - policy = ToolPolicy( - tool_pattern=tool_pattern, required_scopes=required_scopes, action=action - ) - self.add_policy(policy) - - def authorize_tool_call( - self, - tool_name: str, - user_scopes: list[str], - user_claims: dict[str, Any] | None = None, - ) -> tuple[bool, str | None]: - """ - Authorize a tool call - - Returns: - (authorized: bool, reason: Optional[str]) - """ - - logger.debug(f"Authorizing tool '{tool_name}' for user with scopes: {user_scopes}") - - matching_policies = [ - policy for policy in self.policies if policy.matches_tool(tool_name) - ] - - if not matching_policies: - if self.default_action == PolicyAction.ALLOW: - logger.debug(f"No policies found for '{tool_name}', allowing by default") - return True, None - else: - logger.warning(f"No policies found for '{tool_name}', denying by default") - return False, f"No policy found for tool '{tool_name}', default deny" - - # Check for explicit deny policies first - for policy in matching_policies: - if policy.action == PolicyAction.DENY: - if policy.evaluate_scopes(user_scopes): - logger.warning(f"Explicit deny policy matched for '{tool_name}'") - return False, f"Explicit deny policy for tool '{tool_name}'" - - # Check allow policies - allow_policies = [ - p for p in matching_policies if p.action == PolicyAction.ALLOW - ] - - if not allow_policies: - logger.warning(f"No allow policies found for '{tool_name}'") - return False, f"No allow policies found for tool '{tool_name}'" - - for policy in allow_policies: - if policy.evaluate_scopes(user_scopes): - if self._evaluate_conditions(policy.conditions, user_claims): - logger.debug(f"Authorization granted for '{tool_name}'") - return True, None - - logger.warning(f"Insufficient scopes for '{tool_name}'. Required: {[p.required_scopes for p in allow_policies]}, User has: {user_scopes}") - return False, f"Insufficient scopes for tool '{tool_name}'" - - def _evaluate_conditions( - self, - conditions: dict[str, Any] | None, - user_claims: dict[str, Any] | None, - ) -> bool: - """Evaluate additional policy conditions""" - - if not conditions: - return True - - if not user_claims: - logger.debug("No user claims provided, conditions evaluation failed") - return False - - for key, expected_value in conditions.items(): - user_value = user_claims.get(key) - - if isinstance(expected_value, list): - if user_value not in expected_value: - logger.debug(f"Condition failed: {key} = {user_value} not in {expected_value}") - return False - elif user_value != expected_value: - logger.debug(f"Condition failed: {key} = {user_value} != {expected_value}") - return False - - return True - - def get_allowed_tools(self, user_scopes: list[str]) -> list[str]: - """Get list of tool patterns user is allowed to call""" - - allowed_tools = [] - - for policy in self.policies: - if policy.action == PolicyAction.ALLOW and policy.evaluate_scopes( - user_scopes - ): - allowed_tools.append(policy.tool_pattern) - - return allowed_tools - - -def create_turkish_legal_policies() -> PolicyEngine: - """Create policy set for Turkish legal database MCP server""" - - engine = PolicyEngine() - - # Administrative tools (full access) - engine.add_tool_scope_policy(".*", ["mcp:tools:admin"]) - - # Search tools - require read access - engine.add_tool_scope_policy("search.*", ["mcp:tools:read"]) - - # Fetch/get document tools - require read access - engine.add_tool_scope_policy("get_.*", ["mcp:tools:read"]) - engine.add_tool_scope_policy("fetch.*", ["mcp:tools:read"]) - - # Specific Turkish legal database tools - engine.add_tool_scope_policy("search_yargitay.*", ["mcp:tools:read"]) - engine.add_tool_scope_policy("search_danistay.*", ["mcp:tools:read"]) - engine.add_tool_scope_policy("search_anayasa.*", ["mcp:tools:read"]) - engine.add_tool_scope_policy("search_rekabet.*", ["mcp:tools:read"]) - engine.add_tool_scope_policy("search_kik.*", ["mcp:tools:read"]) - engine.add_tool_scope_policy("search_emsal.*", ["mcp:tools:read"]) - engine.add_tool_scope_policy("search_uyusmazlik.*", ["mcp:tools:read"]) - engine.add_tool_scope_policy("search_sayistay.*", ["mcp:tools:read"]) - engine.add_tool_scope_policy("search_.*_bedesten", ["mcp:tools:read"]) - engine.add_tool_scope_policy("search_yerel_hukuk.*", ["mcp:tools:read"]) - engine.add_tool_scope_policy("search_istinaf_hukuk.*", ["mcp:tools:read"]) - engine.add_tool_scope_policy("search_kyb.*", ["mcp:tools:read"]) - - # Document retrieval tools - engine.add_tool_scope_policy("get_.*_document.*", ["mcp:tools:read"]) - engine.add_tool_scope_policy("get_.*_markdown", ["mcp:tools:read"]) - - # Write operations (if any future tools need them) - engine.add_tool_scope_policy("create_.*", ["mcp:tools:write"]) - engine.add_tool_scope_policy("update_.*", ["mcp:tools:write"]) - engine.add_tool_scope_policy("delete_.*", ["mcp:tools:write"]) - - logger.info("Created Turkish legal database policy engine") - return engine - - -def create_default_policies() -> PolicyEngine: - """Create a default policy set for MCP servers (backwards compatibility)""" - return create_turkish_legal_policies() \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/storage.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth/storage.py deleted file mode 100644 index ea1ad8f..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/mcp_auth/storage.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -Persistent storage for OAuth sessions and tokens -""" - -import json -import os -import tempfile -import logging -from datetime import datetime -from typing import Dict, Any, Optional - -logger = logging.getLogger(__name__) - - -class PersistentStorage: - """File-based persistent storage for OAuth data""" - - def __init__(self, storage_dir: str = None): - if storage_dir is None: - # Use system temp directory or environment variable - storage_dir = os.environ.get('TEMP', tempfile.gettempdir()) - - self.storage_dir = os.path.join(storage_dir, 'mcp_oauth_storage') - os.makedirs(self.storage_dir, exist_ok=True) - - self.sessions_file = os.path.join(self.storage_dir, 'oauth_sessions.json') - self.tokens_file = os.path.join(self.storage_dir, 'oauth_tokens.json') - - logger.info(f"Persistent OAuth storage initialized at: {self.storage_dir}") - - def _load_json(self, filepath: str) -> Dict: - """Load JSON data from file""" - try: - if os.path.exists(filepath): - with open(filepath, 'r', encoding='utf-8') as f: - return json.load(f) - except Exception as e: - logger.error(f"Error loading {filepath}: {e}") - return {} - - def _save_json(self, filepath: str, data: Dict): - """Save JSON data to file""" - try: - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=2, default=str) - except Exception as e: - logger.error(f"Error saving {filepath}: {e}") - - def get_sessions(self) -> Dict[str, Dict[str, Any]]: - """Get all OAuth sessions""" - data = self._load_json(self.sessions_file) - # Clean expired sessions - now = datetime.utcnow().timestamp() - valid_sessions = {k: v for k, v in data.items() - if v.get('expires_at', 0) > now} - if len(valid_sessions) != len(data): - self._save_json(self.sessions_file, valid_sessions) - return valid_sessions - - def set_session(self, session_id: str, data: Dict[str, Any]): - """Set OAuth session data""" - sessions = self.get_sessions() - sessions[session_id] = data - self._save_json(self.sessions_file, sessions) - - def get_session(self, session_id: str) -> Optional[Dict[str, Any]]: - """Get specific OAuth session data""" - sessions = self.get_sessions() - return sessions.get(session_id) - - def delete_session(self, session_id: str): - """Delete OAuth session""" - sessions = self.get_sessions() - if session_id in sessions: - del sessions[session_id] - self._save_json(self.sessions_file, sessions) - - def get_tokens(self) -> Dict[str, Dict[str, Any]]: - """Get all OAuth tokens""" - data = self._load_json(self.tokens_file) - # Clean expired tokens - now = datetime.utcnow().timestamp() - valid_tokens = {k: v for k, v in data.items() - if v.get('expires_at', 0) > now} - if len(valid_tokens) != len(data): - self._save_json(self.tokens_file, valid_tokens) - return valid_tokens - - def set_token(self, token_id: str, token_data: Dict[str, Any]): - """Set OAuth token data""" - tokens = self.get_tokens() - tokens[token_id] = token_data - self._save_json(self.tokens_file, tokens) - - def get_token(self, token_id: str) -> Optional[Dict[str, Any]]: - """Get specific OAuth token data""" - tokens = self.get_tokens() - return tokens.get(token_id) - - def delete_token(self, token_id: str): - """Delete OAuth token""" - tokens = self.get_tokens() - if token_id in tokens: - del tokens[token_id] - self._save_json(self.tokens_file, tokens) - - def cleanup_expired_sessions(self): - """Clean up expired sessions and tokens""" - # This is handled automatically in get_sessions() and get_tokens() - sessions = self.get_sessions() - tokens = self.get_tokens() - logger.debug(f"Cleanup: {len(sessions)} active sessions, {len(tokens)} active tokens") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth_factory.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth_factory.py deleted file mode 100644 index a5760f6..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/mcp_auth_factory.py +++ /dev/null @@ -1,193 +0,0 @@ -""" -Factory for creating FastMCP app with MCP Auth Toolkit integration -""" - -import logging -import os -from typing import Optional - -logger = logging.getLogger(__name__) - -try: - from fastmcp import FastMCP - FASTMCP_AVAILABLE = True -except ImportError: - FASTMCP_AVAILABLE = False - FastMCP = None - -from mcp_auth import ( - OAuthProvider, - PolicyEngine, - FastMCPAuthWrapper, - create_default_policies -) -from mcp_auth.clerk_config import create_mcp_server_config - - -def create_auth_enabled_app(app_name: str = "Yargı MCP Server") -> FastMCP: - """Create FastMCP app with authentication enabled""" - - if not FASTMCP_AVAILABLE: - raise ImportError("FastMCP is required for authenticated MCP server") - - logger.info("Creating FastMCP app with MCP Auth Toolkit integration") - - # Create base FastMCP app - app = FastMCP(app_name) - - # Check if authentication is enabled - auth_enabled = os.getenv("ENABLE_AUTH", "true").lower() == "true" - - if not auth_enabled: - logger.info("Authentication disabled, returning basic FastMCP app") - return app - - try: - # Get configuration - logger.info("Getting MCP server configuration...") - config = create_mcp_server_config() - logger.info("Configuration loaded successfully") - - # Create OAuth provider with Clerk config - logger.info("Creating OAuth provider...") - oauth_provider = OAuthProvider( - config=config["oauth_config"], - jwt_secret=config["jwt_secret"] - ) - logger.info("OAuth provider created successfully") - - # Create policy engine for Turkish legal database - policy_engine = create_default_policies() - - # Store auth components for later wrapping (after tools are defined) - app._oauth_provider = oauth_provider - app._policy_engine = policy_engine - app._auth_config = config - - # Add OAuth endpoints immediately - @app.tool( - description="Initiate OAuth 2.1 authorization flow with PKCE", - annotations={"readOnlyHint": True, "idempotentHint": False} - ) - async def oauth_authorize(redirect_uri: str, scopes: str = None): - """OAuth authorization endpoint""" - scope_list = scopes.split(" ") if scopes else ["mcp:tools:read", "mcp:tools:write"] - auth_url, pkce = oauth_provider.generate_authorization_url( - redirect_uri=redirect_uri, scopes=scope_list - ) - logger.info(f"Generated authorization URL for redirect_uri: {redirect_uri}") - return { - "authorization_url": auth_url, - "code_verifier": pkce.verifier, - "code_challenge": pkce.challenge, - "instructions": "Use the authorization_url to complete OAuth flow, then exchange the returned code using oauth_token tool" - } - - @app.tool( - description="Exchange OAuth authorization code for access token", - annotations={"readOnlyHint": False, "idempotentHint": False} - ) - async def oauth_token(code: str, state: str, redirect_uri: str): - """OAuth token exchange endpoint""" - try: - result = await oauth_provider.exchange_code_for_token( - code=code, state=state, redirect_uri=redirect_uri - ) - logger.info("Successfully exchanged authorization code for token") - return result - except Exception as e: - logger.error(f"Token exchange failed: {e}") - raise - - @app.tool( - description="Validate and introspect OAuth access token", - annotations={"readOnlyHint": True, "idempotentHint": True} - ) - async def oauth_introspect(token: str): - """Token introspection endpoint""" - result = oauth_provider.introspect_token(token) - logger.debug(f"Token introspection: active={result.get('active', False)}") - return result - - @app.tool( - description="Revoke OAuth access token", - annotations={"readOnlyHint": False, "idempotentHint": False} - ) - async def oauth_revoke(token: str): - """Token revocation endpoint""" - success = oauth_provider.revoke_token(token) - logger.info(f"Token revocation: success={success}") - return {"revoked": success} - - logger.info("Successfully created authenticated FastMCP app") - - except Exception as e: - logger.error(f"Failed to create authenticated app: {e}") - logger.info("Falling back to non-authenticated FastMCP app") - # Return basic app if auth setup fails - return app - - return app - - -def create_app() -> FastMCP: - """Create FastMCP app (backwards compatible with mcp_factory.py)""" - return create_auth_enabled_app() - - -def get_auth_wrapper(app: FastMCP) -> Optional[FastMCPAuthWrapper]: - """Get auth wrapper from app if available""" - return getattr(app, '_auth_wrapper', None) - - -def get_oauth_provider(app: FastMCP) -> Optional[OAuthProvider]: - """Get OAuth provider from app if available""" - return getattr(app, '_oauth_provider', None) - - -def get_policy_engine(app: FastMCP) -> Optional[PolicyEngine]: - """Get policy engine from app if available""" - return getattr(app, '_policy_engine', None) - - -def is_auth_enabled(app: FastMCP) -> bool: - """Check if authentication is enabled for the app""" - return hasattr(app, '_oauth_provider') or hasattr(app, '_auth_wrapper') - - -def enable_tool_authentication(app: FastMCP): - """Enable authentication on all existing tools (call after tools are defined)""" - if not is_auth_enabled(app): - logger.debug("Authentication not enabled, skipping tool authentication") - return - - oauth_provider = get_oauth_provider(app) - policy_engine = get_policy_engine(app) - - if not oauth_provider or not policy_engine: - logger.warning("OAuth provider or policy engine not available") - return - - try: - # Create auth wrapper and wrap tools - auth_wrapper = FastMCPAuthWrapper( - mcp_server=app, - oauth_provider=oauth_provider, - policy_engine=policy_engine - ) - - # Store wrapper for reference - app._auth_wrapper = auth_wrapper - - logger.info("Tool authentication enabled successfully") - - except Exception as e: - logger.error(f"Failed to enable tool authentication: {e}") - - -def cleanup_auth_sessions(app: FastMCP): - """Clean up expired auth sessions and tokens""" - oauth_provider = get_oauth_provider(app) - if oauth_provider: - oauth_provider.cleanup_expired_sessions() - logger.debug("Cleaned up expired OAuth sessions") \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth_http_adapter.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth_http_adapter.py deleted file mode 100644 index 442b540..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/mcp_auth_http_adapter.py +++ /dev/null @@ -1,383 +0,0 @@ -""" -HTTP adapter for MCP Auth Toolkit OAuth endpoints -Exposes MCP OAuth tools as HTTP endpoints for Claude.ai integration -""" - -import os -import logging -import secrets -import time -from typing import Optional -from urllib.parse import urlencode, quote -from datetime import datetime, timedelta - -from fastapi import APIRouter, Request, Query, HTTPException -from fastapi.responses import RedirectResponse, JSONResponse - -# Try to import Clerk SDK -try: - from clerk_backend_api import Clerk - CLERK_AVAILABLE = True -except ImportError as e: - CLERK_AVAILABLE = False - Clerk = None - -logger = logging.getLogger(__name__) - -router = APIRouter() - -# OAuth configuration -BASE_URL = os.getenv("BASE_URL", "https://yargimcp.com") - - -@router.get("/.well-known/oauth-authorization-server") -async def get_oauth_metadata(): - """OAuth 2.0 Authorization Server Metadata (RFC 8414)""" - return JSONResponse({ - "issuer": BASE_URL, - "authorization_endpoint": f"{BASE_URL}/authorize", - "token_endpoint": f"{BASE_URL}/token", - "registration_endpoint": f"{BASE_URL}/register", - "response_types_supported": ["code"], - "grant_types_supported": ["authorization_code", "refresh_token"], - "code_challenge_methods_supported": ["S256"], - "token_endpoint_auth_methods_supported": ["none"], - "scopes_supported": ["mcp:tools:read", "mcp:tools:write", "openid", "profile", "email"], - "service_documentation": f"{BASE_URL}/mcp/" - }) - - -@router.get("/.well-known/oauth-protected-resource") -async def get_protected_resource_metadata(): - """OAuth Protected Resource Metadata (RFC 9728)""" - return JSONResponse({ - "resource": BASE_URL, - "authorization_servers": [BASE_URL], - "bearer_methods_supported": ["header"], - "scopes_supported": ["mcp:tools:read", "mcp:tools:write"], - "resource_documentation": f"{BASE_URL}/docs" - }) - - -@router.get("/authorize") -async def authorize_endpoint( - response_type: str = Query(...), - client_id: str = Query(...), - redirect_uri: str = Query(...), - code_challenge: str = Query(...), - code_challenge_method: str = Query("S256"), - state: Optional[str] = Query(None), - scope: Optional[str] = Query(None) -): - """OAuth 2.1 Authorization Endpoint - Uses Clerk SDK for custom domains""" - - logger.info(f"OAuth authorize request - client_id: {client_id}, redirect_uri: {redirect_uri}") - - if not CLERK_AVAILABLE: - logger.error("Clerk SDK not available") - raise HTTPException(status_code=500, detail="Clerk SDK not available") - - # Store OAuth session for later validation - try: - from mcp_server_main import app as mcp_app - from mcp_auth_factory import get_oauth_provider - - oauth_provider = get_oauth_provider(mcp_app) - if not oauth_provider: - raise HTTPException(status_code=500, detail="OAuth provider not configured") - - # Generate session and store PKCE - session_id = secrets.token_urlsafe(32) - if state is None: - state = secrets.token_urlsafe(16) - - # Create PKCE challenge - from mcp_auth.oauth import PKCEChallenge - pkce = PKCEChallenge() - - # Store session data - session_data = { - "pkce_verifier": pkce.verifier, - "pkce_challenge": code_challenge, # Store the client's challenge - "state": state, - "redirect_uri": redirect_uri, - "client_id": client_id, - "scopes": scope.split(" ") if scope else ["mcp:tools:read", "mcp:tools:write"], - "created_at": time.time(), - "expires_at": (datetime.utcnow() + timedelta(minutes=10)).timestamp(), - } - oauth_provider.storage.set_session(session_id, session_data) - - # For Clerk with custom domains, we need to use their hosted sign-in page - # We'll pass our callback URL and session info in the state - callback_url = f"{BASE_URL}/auth/callback" - - # Encode session info in state for retrieval after Clerk auth - combined_state = f"{state}:{session_id}" - - # Use Clerk's sign-in URL with proper parameters - clerk_domain = os.getenv("CLERK_DOMAIN", "accounts.yargimcp.com") - sign_in_params = { - "redirect_url": f"{callback_url}?state={quote(combined_state)}", - } - - sign_in_url = f"https://{clerk_domain}/sign-in?{urlencode(sign_in_params)}" - - logger.info(f"Redirecting to Clerk sign-in: {sign_in_url}") - - return RedirectResponse(url=sign_in_url) - - except Exception as e: - logger.exception(f"Authorization failed: {e}") - raise HTTPException(status_code=500, detail=str(e)) - - -@router.get("/auth/callback") -async def oauth_callback( - request: Request, - state: Optional[str] = Query(None), - clerk_token: Optional[str] = Query(None) -): - """Handle OAuth callback from Clerk - supports both JWT token and cookie auth""" - - logger.info(f"OAuth callback received - state: {state}") - logger.info(f"Query params: {dict(request.query_params)}") - logger.info(f"Cookies: {dict(request.cookies)}") - logger.info(f"Clerk JWT token provided: {bool(clerk_token)}") - - # Support both JWT token (for cross-domain) and cookie auth (for subdomain) - - try: - if not state: - logger.error("No state parameter provided") - return JSONResponse( - status_code=400, - content={"error": "invalid_request", "error_description": "Missing state parameter"} - ) - - # Parse state to get original state and session ID - try: - if ":" in state: - original_state, session_id = state.rsplit(":", 1) - else: - original_state = state - session_id = state # Fallback - except ValueError: - logger.error(f"Invalid state format: {state}") - return JSONResponse( - status_code=400, - content={"error": "invalid_request", "error_description": "Invalid state format"} - ) - - # Get OAuth provider - from mcp_server_main import app as mcp_app - from mcp_auth_factory import get_oauth_provider - - oauth_provider = get_oauth_provider(mcp_app) - if not oauth_provider: - raise HTTPException(status_code=500, detail="OAuth provider not configured") - - # Get stored session - oauth_session = oauth_provider.storage.get_session(session_id) - - if not oauth_session: - logger.error(f"OAuth session not found for ID: {session_id}") - return JSONResponse( - status_code=400, - content={"error": "invalid_request", "error_description": "OAuth session expired or not found"} - ) - - # Check if we have a JWT token (for cross-domain auth) - user_authenticated = False - auth_method = "none" - - if clerk_token: - logger.info("Attempting JWT token validation") - try: - # Validate JWT token with Clerk - from clerk_backend_api import Clerk - clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY")) - - # Extract session_id from JWT token and verify with Clerk - import jwt - decoded_token = jwt.decode(clerk_token, options={"verify_signature": False}) - session_id = decoded_token.get("sid") or decoded_token.get("session_id") - - if session_id: - # Verify with Clerk using session_id - session = clerk.sessions.verify(session_id=session_id, token=clerk_token) - user_id = session.user_id if session else None - else: - user_id = None - - if user_id: - logger.info(f"JWT token validation successful - user_id: {user_id}") - user_authenticated = True - auth_method = "jwt_token" - # Store user info in session for token exchange - oauth_session["user_id"] = user_id - oauth_session["auth_method"] = "jwt_token" - else: - logger.error("JWT token validation failed - no user_id in claims") - except Exception as e: - logger.error(f"JWT token validation failed: {str(e)}") - # Fall through to cookie validation - - # If no JWT token or validation failed, check cookies - if not user_authenticated: - logger.info("Checking for Clerk session cookies") - # Check for Clerk session cookies (for subdomain auth) - clerk_session_cookie = request.cookies.get("__session") - if clerk_session_cookie: - logger.info("Found Clerk session cookie, assuming authenticated") - user_authenticated = True - auth_method = "cookie" - oauth_session["auth_method"] = "cookie" - else: - logger.info("No Clerk session cookie found") - - # For custom domains, we'll also trust that Clerk redirected here - if not user_authenticated: - logger.info("Trusting Clerk redirect for custom domain flow") - user_authenticated = True - auth_method = "trusted_redirect" - oauth_session["auth_method"] = "trusted_redirect" - - logger.info(f"User authenticated: {user_authenticated}, method: {auth_method}") - - # Generate simple authorization code for custom domain flow - auth_code = f"clerk_custom_{session_id}_{int(time.time())}" - - # Store the code mapping for token exchange - code_data = { - "session_id": session_id, - "clerk_authenticated": user_authenticated, - "auth_method": auth_method, - "custom_domain_flow": True, - "created_at": time.time(), - "expires_at": (datetime.utcnow() + timedelta(minutes=5)).timestamp(), - } - if "user_id" in oauth_session: - code_data["user_id"] = oauth_session["user_id"] - - oauth_provider.storage.set_session(f"code_{auth_code}", code_data) - - # Build redirect URL back to Claude - redirect_params = { - "code": auth_code, - "state": original_state - } - - redirect_url = f"{oauth_session['redirect_uri']}?{urlencode(redirect_params)}" - logger.info(f"Redirecting back to Claude: {redirect_url}") - - return RedirectResponse(url=redirect_url) - - except Exception as e: - logger.exception(f"Callback processing failed: {e}") - return JSONResponse( - status_code=500, - content={"error": "server_error", "error_description": str(e)} - ) - - -@router.post("/register") -async def register_client(request: Request): - """Dynamic Client Registration (RFC 7591)""" - - data = await request.json() - logger.info(f"Client registration request: {data}") - - # Simple dynamic registration - accept any client - client_id = f"mcp-client-{os.urandom(8).hex()}" - - return JSONResponse({ - "client_id": client_id, - "client_secret": None, # Public client - "redirect_uris": data.get("redirect_uris", []), - "grant_types": ["authorization_code", "refresh_token"], - "response_types": ["code"], - "client_name": data.get("client_name", "MCP Client"), - "token_endpoint_auth_method": "none", - "client_id_issued_at": int(datetime.now().timestamp()) - }) - - -@router.post("/token") -async def token_endpoint(request: Request): - """OAuth 2.1 Token Endpoint""" - - # Parse form data - form_data = await request.form() - grant_type = form_data.get("grant_type") - code = form_data.get("code") - redirect_uri = form_data.get("redirect_uri") - client_id = form_data.get("client_id") - code_verifier = form_data.get("code_verifier") - - logger.info(f"Token exchange - grant_type: {grant_type}, code: {code[:20] if code else 'None'}...") - - if grant_type != "authorization_code": - return JSONResponse( - status_code=400, - content={"error": "unsupported_grant_type"} - ) - - try: - # OAuth token exchange - validate code and return Clerk JWT - # This supports proper OAuth flow while using Clerk JWT tokens - - if not code or not redirect_uri: - logger.error("Missing required parameters: code or redirect_uri") - return JSONResponse( - status_code=400, - content={"error": "invalid_request", "error_description": "Missing code or redirect_uri"} - ) - - # Validate OAuth code with Clerk - if CLERK_AVAILABLE: - try: - clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY")) - - # In a real implementation, you'd validate the code with Clerk - # For now, we'll assume the code is valid if it looks like a Clerk code - if len(code) > 10: # Basic validation - # Create a mock session with the code - # In practice, this would be validated with Clerk's OAuth flow - - # Return Clerk JWT token format - # This should be the actual Clerk JWT token from the OAuth flow - return JSONResponse({ - "access_token": f"mock_clerk_jwt_{code}", - "token_type": "Bearer", - "expires_in": 3600, - "scope": "yargi.read yargi.search" - }) - else: - logger.error(f"Invalid code format: {code}") - return JSONResponse( - status_code=400, - content={"error": "invalid_grant", "error_description": "Invalid authorization code"} - ) - - except Exception as e: - logger.error(f"Clerk validation failed: {e}") - return JSONResponse( - status_code=400, - content={"error": "invalid_grant", "error_description": "Authorization code validation failed"} - ) - else: - logger.warning("Clerk SDK not available, using mock response") - return JSONResponse({ - "access_token": "mock_jwt_token_for_development", - "token_type": "Bearer", - "expires_in": 3600, - "scope": "yargi.read yargi.search" - }) - - except Exception as e: - logger.exception(f"Token exchange failed: {e}") - return JSONResponse( - status_code=500, - content={"error": "server_error", "error_description": str(e)} - ) \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_auth_http_simple.py b/saidsurucu-yargi-mcp-f5fa007/mcp_auth_http_simple.py deleted file mode 100644 index 4311ef9..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/mcp_auth_http_simple.py +++ /dev/null @@ -1,522 +0,0 @@ -""" -Simplified MCP OAuth HTTP adapter - only Clerk JWT based authentication -Uses Redis for authorization code storage to support multi-machine deployment -""" - -import os -import logging -from typing import Optional -from urllib.parse import urlencode, quote - -from fastapi import APIRouter, Request, Query, HTTPException -from fastapi.responses import RedirectResponse, JSONResponse - -# Import Redis session store -from redis_session_store import get_redis_store - -# Try to import Clerk SDK -try: - from clerk_backend_api import Clerk - CLERK_AVAILABLE = True -except ImportError: - CLERK_AVAILABLE = False - Clerk = None - -logger = logging.getLogger(__name__) - -router = APIRouter() - -# OAuth configuration -BASE_URL = os.getenv("BASE_URL", "https://api.yargimcp.com") -CLERK_DOMAIN = os.getenv("CLERK_DOMAIN", "accounts.yargimcp.com") - -# Initialize Redis store -redis_store = None - -def get_redis_session_store(): - """Get Redis store instance with lazy initialization.""" - global redis_store - if redis_store is None: - try: - import concurrent.futures - import functools - - # Use thread pool with timeout to prevent hanging - with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: - future = executor.submit(get_redis_store) - try: - # 5 second timeout for Redis initialization - redis_store = future.result(timeout=5.0) - if redis_store: - logger.info("Redis session store initialized for OAuth handler") - else: - logger.warning("Redis store initialization returned None") - except concurrent.futures.TimeoutError: - logger.error("Redis initialization timed out after 5 seconds") - redis_store = None - future.cancel() # Try to cancel the hanging operation - - except Exception as e: - logger.error(f"Failed to initialize Redis store: {e}") - redis_store = None - - if redis_store is None: - # Fall back to in-memory storage with warning - logger.warning("Falling back to in-memory storage - multi-machine deployment will not work") - - return redis_store - -@router.get("/.well-known/oauth-authorization-server") -async def get_oauth_metadata(): - """OAuth 2.0 Authorization Server Metadata (RFC 8414)""" - return JSONResponse({ - "issuer": BASE_URL, - "authorization_endpoint": "https://yargimcp.com/mcp-callback", - "token_endpoint": f"{BASE_URL}/token", - "registration_endpoint": f"{BASE_URL}/register", - "response_types_supported": ["code"], - "grant_types_supported": ["authorization_code"], - "code_challenge_methods_supported": ["S256"], - "token_endpoint_auth_methods_supported": ["none"], - "scopes_supported": ["read", "search", "openid", "profile", "email"], - "service_documentation": f"{BASE_URL}/mcp/" - }) - -@router.get("/auth/login") -async def oauth_authorize( - request: Request, - client_id: str = Query(...), - redirect_uri: str = Query(...), - response_type: str = Query("code"), - scope: Optional[str] = Query("read search"), - state: Optional[str] = Query(None), - code_challenge: Optional[str] = Query(None), - code_challenge_method: Optional[str] = Query(None) -): - """OAuth 2.1 Authorization Endpoint - redirects to Clerk""" - - logger.info(f"OAuth authorize request - client_id: {client_id}") - logger.info(f"Redirect URI: {redirect_uri}") - logger.info(f"State: {state}") - logger.info(f"PKCE Challenge: {bool(code_challenge)}") - - try: - # Build callback URL with all necessary parameters - callback_url = f"{BASE_URL}/auth/callback" - callback_params = { - "client_id": client_id, - "redirect_uri": redirect_uri, - "state": state or "", - "scope": scope or "read search" - } - - # Add PKCE parameters if present - if code_challenge: - callback_params["code_challenge"] = code_challenge - callback_params["code_challenge_method"] = code_challenge_method or "S256" - - # Encode callback URL as redirect_url for Clerk - callback_with_params = f"{callback_url}?{urlencode(callback_params)}" - - # Build Clerk sign-in URL - use yargimcp.com frontend for JWT token generation - clerk_params = { - "redirect_url": callback_with_params - } - - # Use frontend sign-in page that handles JWT token generation - clerk_signin_url = f"https://yargimcp.com/sign-in?{urlencode(clerk_params)}" - - logger.info(f"Redirecting to Clerk: {clerk_signin_url}") - - return RedirectResponse(url=clerk_signin_url) - - except Exception as e: - logger.exception(f"Authorization failed: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@router.get("/auth/callback") -async def oauth_callback( - request: Request, - client_id: str = Query(...), - redirect_uri: str = Query(...), - state: Optional[str] = Query(None), - scope: Optional[str] = Query("read search"), - code_challenge: Optional[str] = Query(None), - code_challenge_method: Optional[str] = Query(None), - clerk_token: Optional[str] = Query(None) -): - """OAuth callback from Clerk - generates authorization code""" - - logger.info(f"OAuth callback - client_id: {client_id}") - logger.info(f"Clerk token provided: {bool(clerk_token)}") - - try: - # Validate user with Clerk and generate real JWT token - user_authenticated = False - user_id = None - session_id = None - real_jwt_token = None - - if clerk_token and CLERK_AVAILABLE: - try: - # Extract user info from JWT token (no Clerk session verification needed) - import jwt - decoded_token = jwt.decode(clerk_token, options={"verify_signature": False}) - user_id = decoded_token.get("user_id") or decoded_token.get("sub") - user_email = decoded_token.get("email") - token_scopes = decoded_token.get("scopes", ["read", "search"]) - - logger.info(f"JWT token claims - user_id: {user_id}, email: {user_email}, scopes: {token_scopes}") - - if user_id and user_email: - # JWT token is already signed by Clerk and contains valid user info - user_authenticated = True - logger.info(f"User authenticated via JWT token - user_id: {user_id}") - - # Use the JWT token directly as the real token (it's already from Clerk template) - real_jwt_token = clerk_token - logger.info("Using Clerk JWT token directly (already real token)") - - else: - logger.error(f"Missing required fields in JWT token - user_id: {bool(user_id)}, email: {bool(user_email)}") - - except Exception as e: - logger.error(f"JWT validation failed: {e}") - - # Fallback to cookie validation - if not user_authenticated: - clerk_session = request.cookies.get("__session") - if clerk_session: - user_authenticated = True - logger.info("User authenticated via cookie") - - # Try to get session from cookie and generate JWT - if CLERK_AVAILABLE: - try: - clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY")) - # Note: sessions.verify_session is deprecated, but we'll try - # In practice, you'd need to extract session_id from cookie - logger.info("Cookie authentication - JWT generation not implemented yet") - except Exception as e: - logger.warning(f"Failed to generate JWT from cookie: {e}") - - # Only generate authorization code if we have a real JWT token - if user_authenticated and real_jwt_token: - # Generate authorization code - auth_code = f"clerk_auth_{os.urandom(16).hex()}" - - # Prepare code data - import time - code_data = { - "user_id": user_id, - "session_id": session_id, - "real_jwt_token": real_jwt_token, - "user_authenticated": user_authenticated, - "client_id": client_id, - "redirect_uri": redirect_uri, - "scope": scope or "read search" - } - - # Try to store in Redis, fall back to in-memory if Redis unavailable - store = get_redis_session_store() - if store: - # Store in Redis with automatic expiration - success = store.set_oauth_code(auth_code, code_data) - if success: - logger.info(f"Stored authorization code {auth_code[:10]}... in Redis with real JWT token") - else: - logger.error(f"Failed to store authorization code in Redis, falling back to in-memory") - # Fall back to in-memory storage - if not hasattr(oauth_callback, '_code_storage'): - oauth_callback._code_storage = {} - oauth_callback._code_storage[auth_code] = code_data - else: - # Fall back to in-memory storage - logger.warning("Redis not available, using in-memory storage") - if not hasattr(oauth_callback, '_code_storage'): - oauth_callback._code_storage = {} - oauth_callback._code_storage[auth_code] = code_data - logger.info(f"Stored authorization code in memory (fallback)") - - # Redirect back to client with authorization code - redirect_params = { - "code": auth_code, - "state": state or "" - } - - final_redirect_url = f"{redirect_uri}?{urlencode(redirect_params)}" - logger.info(f"Redirecting back to client: {final_redirect_url}") - - return RedirectResponse(url=final_redirect_url) - else: - # No JWT token yet - redirect back to sign-in page to wait for authentication - logger.info("No JWT token provided - redirecting back to sign-in to complete authentication") - - # Keep the same redirect URL so the flow continues - sign_in_params = { - "redirect_url": f"{request.url._url}" # Current callback URL with all params - } - - sign_in_url = f"https://yargimcp.com/sign-in?{urlencode(sign_in_params)}" - logger.info(f"Redirecting back to sign-in: {sign_in_url}") - - return RedirectResponse(url=sign_in_url) - - except Exception as e: - logger.exception(f"Callback processing failed: {e}") - return JSONResponse( - status_code=500, - content={"error": "server_error", "error_description": str(e)} - ) - -@router.post("/auth/register") -async def register_client(request: Request): - """Dynamic Client Registration (RFC 7591)""" - - data = await request.json() - logger.info(f"Client registration request: {data}") - - # Simple dynamic registration - accept any client - client_id = f"mcp-client-{os.urandom(8).hex()}" - - return JSONResponse({ - "client_id": client_id, - "client_secret": None, # Public client - "redirect_uris": data.get("redirect_uris", []), - "grant_types": ["authorization_code"], - "response_types": ["code"], - "client_name": data.get("client_name", "MCP Client"), - "token_endpoint_auth_method": "none" - }) - -@router.post("/auth/callback") -async def oauth_callback_post(request: Request): - """OAuth callback POST endpoint for token exchange""" - - # Parse form data (standard OAuth token exchange format) - form_data = await request.form() - grant_type = form_data.get("grant_type") - code = form_data.get("code") - redirect_uri = form_data.get("redirect_uri") - client_id = form_data.get("client_id") - code_verifier = form_data.get("code_verifier") - - logger.info(f"OAuth callback POST - grant_type: {grant_type}") - logger.info(f"Code: {code[:20] if code else 'None'}...") - logger.info(f"Client ID: {client_id}") - logger.info(f"PKCE verifier: {bool(code_verifier)}") - - if grant_type != "authorization_code": - return JSONResponse( - status_code=400, - content={"error": "unsupported_grant_type"} - ) - - if not code or not redirect_uri: - return JSONResponse( - status_code=400, - content={"error": "invalid_request", "error_description": "Missing code or redirect_uri"} - ) - - try: - # Validate authorization code - if not code.startswith("clerk_auth_"): - return JSONResponse( - status_code=400, - content={"error": "invalid_grant", "error_description": "Invalid authorization code"} - ) - - # Retrieve stored JWT token using authorization code from Redis or in-memory fallback - stored_code_data = None - - # Try to get from Redis first, then fall back to in-memory - store = get_redis_session_store() - if store: - stored_code_data = store.get_oauth_code(code, delete_after_use=True) - if stored_code_data: - logger.info(f"Retrieved authorization code {code[:10]}... from Redis") - else: - logger.warning(f"Authorization code {code[:10]}... not found in Redis") - - # Fall back to in-memory storage if Redis unavailable or code not found - if not stored_code_data and hasattr(oauth_callback, '_code_storage'): - stored_code_data = oauth_callback._code_storage.get(code) - if stored_code_data: - # Clean up in-memory storage - oauth_callback._code_storage.pop(code, None) - logger.info(f"Retrieved authorization code {code[:10]}... from in-memory storage") - - if not stored_code_data: - logger.error(f"No stored data found for authorization code: {code}") - return JSONResponse( - status_code=400, - content={"error": "invalid_grant", "error_description": "Authorization code not found or expired"} - ) - - # Note: Redis TTL handles expiration automatically, but check for manual expiration for in-memory fallback - import time - expires_at = stored_code_data.get("expires_at", 0) - if expires_at and time.time() > expires_at: - logger.error(f"Authorization code expired: {code}") - return JSONResponse( - status_code=400, - content={"error": "invalid_grant", "error_description": "Authorization code expired"} - ) - - # Get the real JWT token - real_jwt_token = stored_code_data.get("real_jwt_token") - - if real_jwt_token: - logger.info("Returning real Clerk JWT token") - # Note: Code already deleted from Redis, clean up in-memory fallback if used - if hasattr(oauth_callback, '_code_storage'): - oauth_callback._code_storage.pop(code, None) - - return JSONResponse({ - "access_token": real_jwt_token, - "token_type": "Bearer", - "expires_in": 3600, - "scope": "read search" - }) - else: - logger.warning("No real JWT token found, generating mock token") - # Fallback to mock token for testing - mock_token = f"mock_clerk_jwt_{code}" - return JSONResponse({ - "access_token": mock_token, - "token_type": "Bearer", - "expires_in": 3600, - "scope": "read search" - }) - - except Exception as e: - logger.exception(f"OAuth callback POST failed: {e}") - return JSONResponse( - status_code=500, - content={"error": "server_error", "error_description": str(e)} - ) - -@router.post("/register") -async def register_client(request: Request): - """Dynamic Client Registration (RFC 7591)""" - - data = await request.json() - logger.info(f"Client registration request: {data}") - - # Simple dynamic registration - accept any client - client_id = f"mcp-client-{os.urandom(8).hex()}" - - return JSONResponse({ - "client_id": client_id, - "client_secret": None, # Public client - "redirect_uris": data.get("redirect_uris", []), - "grant_types": ["authorization_code"], - "response_types": ["code"], - "client_name": data.get("client_name", "MCP Client"), - "token_endpoint_auth_method": "none" - }) - -@router.post("/token") -async def token_endpoint(request: Request): - """OAuth 2.1 Token Endpoint - exchanges code for Clerk JWT""" - - # Parse form data - form_data = await request.form() - grant_type = form_data.get("grant_type") - code = form_data.get("code") - redirect_uri = form_data.get("redirect_uri") - client_id = form_data.get("client_id") - code_verifier = form_data.get("code_verifier") - - logger.info(f"Token exchange - grant_type: {grant_type}") - logger.info(f"Code: {code[:20] if code else 'None'}...") - - if grant_type != "authorization_code": - return JSONResponse( - status_code=400, - content={"error": "unsupported_grant_type"} - ) - - if not code or not redirect_uri: - return JSONResponse( - status_code=400, - content={"error": "invalid_request", "error_description": "Missing code or redirect_uri"} - ) - - try: - # Validate authorization code - if not code.startswith("clerk_auth_"): - return JSONResponse( - status_code=400, - content={"error": "invalid_grant", "error_description": "Invalid authorization code"} - ) - - # Retrieve stored JWT token using authorization code from Redis or in-memory fallback - stored_code_data = None - - # Try to get from Redis first, then fall back to in-memory - store = get_redis_session_store() - if store: - stored_code_data = store.get_oauth_code(code, delete_after_use=True) - if stored_code_data: - logger.info(f"Retrieved authorization code {code[:10]}... from Redis (/token endpoint)") - else: - logger.warning(f"Authorization code {code[:10]}... not found in Redis (/token endpoint)") - - # Fall back to in-memory storage if Redis unavailable or code not found - if not stored_code_data and hasattr(oauth_callback, '_code_storage'): - stored_code_data = oauth_callback._code_storage.get(code) - if stored_code_data: - # Clean up in-memory storage - oauth_callback._code_storage.pop(code, None) - logger.info(f"Retrieved authorization code {code[:10]}... from in-memory storage (/token endpoint)") - - if not stored_code_data: - logger.error(f"No stored data found for authorization code: {code}") - return JSONResponse( - status_code=400, - content={"error": "invalid_grant", "error_description": "Authorization code not found or expired"} - ) - - # Note: Redis TTL handles expiration automatically, but check for manual expiration for in-memory fallback - import time - expires_at = stored_code_data.get("expires_at", 0) - if expires_at and time.time() > expires_at: - logger.error(f"Authorization code expired: {code}") - return JSONResponse( - status_code=400, - content={"error": "invalid_grant", "error_description": "Authorization code expired"} - ) - - # Get the real JWT token - real_jwt_token = stored_code_data.get("real_jwt_token") - - if real_jwt_token: - logger.info("Returning real Clerk JWT token from /token endpoint") - # Note: Code already deleted from Redis, clean up in-memory fallback if used - if hasattr(oauth_callback, '_code_storage'): - oauth_callback._code_storage.pop(code, None) - - return JSONResponse({ - "access_token": real_jwt_token, - "token_type": "Bearer", - "expires_in": 3600, - "scope": "read search" - }) - else: - logger.warning("No real JWT token found in /token endpoint, generating mock token") - # Fallback to mock token for testing - mock_token = f"mock_clerk_jwt_{code}" - return JSONResponse({ - "access_token": mock_token, - "token_type": "Bearer", - "expires_in": 3600, - "scope": "read search" - }) - - except Exception as e: - logger.exception(f"Token exchange failed: {e}") - return JSONResponse( - status_code=500, - content={"error": "server_error", "error_description": str(e)} - ) \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/mcp_server_main.py b/saidsurucu-yargi-mcp-f5fa007/mcp_server_main.py deleted file mode 100644 index 7c3e512..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/mcp_server_main.py +++ /dev/null @@ -1,2883 +0,0 @@ -# mcp_server_main.py -import asyncio -import atexit -import logging -import os -import httpx -import json -import time -from collections import defaultdict -from pydantic import HttpUrl, Field -from typing import Optional, Dict, List, Literal, Any, Union -import urllib.parse -import tiktoken -from fastmcp.server.middleware import Middleware, MiddlewareContext -from fastmcp.server.dependencies import get_access_token, AccessToken -from fastmcp import Context - -# Use standard exception for tool errors -class ToolError(Exception): - """Tool execution error""" - pass - -# --- Logging Configuration Start --- -LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") -if not os.path.exists(LOG_DIRECTORY): - os.makedirs(LOG_DIRECTORY) -LOG_FILE_PATH = os.path.join(LOG_DIRECTORY, "mcp_server.log") - -root_logger = logging.getLogger() -root_logger.setLevel(logging.DEBUG) - -log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(threadName)s - %(message)s') - -file_handler = logging.FileHandler(LOG_FILE_PATH, mode='a', encoding='utf-8') -file_handler.setFormatter(log_formatter) -file_handler.setLevel(logging.DEBUG) -root_logger.addHandler(file_handler) - -console_handler = logging.StreamHandler() -console_handler.setFormatter(log_formatter) -console_handler.setLevel(logging.INFO) -root_logger.addHandler(console_handler) - -logger = logging.getLogger(__name__) -# --- Logging Configuration End --- - -# --- Token Counting Middleware --- -class TokenCountingMiddleware(Middleware): - """Middleware for counting input/output tokens using tiktoken.""" - - def __init__(self, model: str = "cl100k_base"): - """Initialize token counting middleware. - - Args: - model: Tiktoken model name (cl100k_base for GPT-4/Claude compatibility) - """ - self.encoder = tiktoken.get_encoding(model) - self.model = model - self.token_stats = defaultdict(lambda: {"input": 0, "output": 0, "calls": 0}) - self.logger = logging.getLogger("token_counter") - - # Create separate log file for token metrics - token_log_path = os.path.join(LOG_DIRECTORY, "token_metrics.log") - token_handler = logging.FileHandler(token_log_path, mode='a', encoding='utf-8') - token_formatter = logging.Formatter('%(asctime)s - %(message)s') - token_handler.setFormatter(token_formatter) - token_handler.setLevel(logging.INFO) - self.logger.addHandler(token_handler) - self.logger.setLevel(logging.INFO) - - def count_tokens(self, text: str) -> int: - """Count tokens in text using tiktoken.""" - if not text: - return 0 - try: - return len(self.encoder.encode(str(text))) - except Exception as e: - logger.warning(f"Token counting failed: {e}") - return 0 - - def extract_text_content(self, data: Any) -> str: - """Extract text content from various data types.""" - if isinstance(data, str): - return data - elif isinstance(data, dict): - # Extract text from common response fields - text_parts = [] - for key, value in data.items(): - if isinstance(value, str): - text_parts.append(value) - elif isinstance(value, list): - for item in value: - if isinstance(item, str): - text_parts.append(item) - elif isinstance(item, dict) and 'text' in item: - text_parts.append(str(item['text'])) - return ' '.join(text_parts) - elif isinstance(data, list): - text_parts = [] - for item in data: - text_parts.append(self.extract_text_content(item)) - return ' '.join(text_parts) - else: - return str(data) - - def log_token_usage(self, operation: str, input_tokens: int, output_tokens: int, - tool_name: str = None, duration_ms: float = None): - """Log token usage with structured format.""" - log_data = { - "operation": operation, - "tool_name": tool_name, - "input_tokens": input_tokens, - "output_tokens": output_tokens, - "total_tokens": input_tokens + output_tokens, - "duration_ms": duration_ms, - "timestamp": time.time() - } - - # Update statistics - key = tool_name if tool_name else operation - self.token_stats[key]["input"] += input_tokens - self.token_stats[key]["output"] += output_tokens - self.token_stats[key]["calls"] += 1 - - # Log as JSON for easy parsing - self.logger.info(json.dumps(log_data)) - - # Also log human-readable format to main logger - logger.info(f"Token Usage - {operation}" + - (f" ({tool_name})" if tool_name else "") + - f": {input_tokens} in + {output_tokens} out = {input_tokens + output_tokens} total") - - async def on_call_tool(self, context: MiddlewareContext, call_next): - """Count tokens for tool calls.""" - start_time = time.perf_counter() - - # Extract tool name and arguments - tool_name = getattr(context.message, 'name', 'unknown_tool') - tool_args = getattr(context.message, 'arguments', {}) - - # Count input tokens (tool arguments) - input_text = self.extract_text_content(tool_args) - input_tokens = self.count_tokens(input_text) - - try: - # Execute the tool - result = await call_next(context) - - # Count output tokens (tool result) - output_text = self.extract_text_content(result) - output_tokens = self.count_tokens(output_text) - - # Calculate duration - duration_ms = (time.perf_counter() - start_time) * 1000 - - # Log token usage - self.log_token_usage("tool_call", input_tokens, output_tokens, - tool_name, duration_ms) - - return result - - except Exception as e: - duration_ms = (time.perf_counter() - start_time) * 1000 - self.log_token_usage("tool_call_error", input_tokens, 0, - tool_name, duration_ms) - raise - - async def on_read_resource(self, context: MiddlewareContext, call_next): - """Count tokens for resource reads.""" - start_time = time.perf_counter() - - # Extract resource URI - resource_uri = getattr(context.message, 'uri', 'unknown_resource') - - try: - # Execute the resource read - result = await call_next(context) - - # Count output tokens (resource content) - output_text = self.extract_text_content(result) - output_tokens = self.count_tokens(output_text) - - # Calculate duration - duration_ms = (time.perf_counter() - start_time) * 1000 - - # Log token usage (no input tokens for resource reads) - self.log_token_usage("resource_read", 0, output_tokens, - resource_uri, duration_ms) - - return result - - except Exception as e: - duration_ms = (time.perf_counter() - start_time) * 1000 - self.log_token_usage("resource_read_error", 0, 0, - resource_uri, duration_ms) - raise - - async def on_get_prompt(self, context: MiddlewareContext, call_next): - """Count tokens for prompt retrievals.""" - start_time = time.perf_counter() - - # Extract prompt name - prompt_name = getattr(context.message, 'name', 'unknown_prompt') - - try: - # Execute the prompt retrieval - result = await call_next(context) - - # Count output tokens (prompt content) - output_text = self.extract_text_content(result) - output_tokens = self.count_tokens(output_text) - - # Calculate duration - duration_ms = (time.perf_counter() - start_time) * 1000 - - # Log token usage - self.log_token_usage("prompt_get", 0, output_tokens, - prompt_name, duration_ms) - - return result - - except Exception as e: - duration_ms = (time.perf_counter() - start_time) * 1000 - self.log_token_usage("prompt_get_error", 0, 0, - prompt_name, duration_ms) - raise - - def get_token_stats(self) -> Dict[str, Any]: - """Get current token usage statistics.""" - return dict(self.token_stats) - - def reset_token_stats(self): - """Reset token usage statistics.""" - self.token_stats.clear() - -# --- End Token Counting Middleware --- - -# Create FastMCP app directly without authentication wrapper -from fastmcp import FastMCP - -def create_app(auth=None): - """Create FastMCP app with standard capabilities and optional auth.""" - global app - if auth: - # Set auth on existing app instead of creating new one - app.auth = auth - app.name = "Yargı MCP Server" - logger.info("MCP server created with Bearer authentication enabled") - else: - # Update placeholder app name only - app.name = "Yargı MCP Server" - logger.info("MCP server created with standard capabilities (FastMCP handles tools.listChanged automatically)") - - # Add token counting middleware - token_counter = TokenCountingMiddleware() - app.add_middleware(token_counter) - logger.info("Token counting middleware added to MCP server") - - return app - -# --- Module Imports --- -from yargitay_mcp_module.client import YargitayOfficialApiClient -from yargitay_mcp_module.models import ( - YargitayDetailedSearchRequest, YargitayDocumentMarkdown, CompactYargitaySearchResult, - YargitayBirimEnum, CleanYargitayDecisionEntry -) -from bedesten_mcp_module.client import BedestenApiClient -from bedesten_mcp_module.models import ( - BedestenSearchRequest, BedestenSearchData, - BedestenDocumentMarkdown, BedestenCourtTypeEnum -) -from bedesten_mcp_module.enums import BirimAdiEnum -from danistay_mcp_module.client import DanistayApiClient -from danistay_mcp_module.models import ( - DanistayKeywordSearchRequest, DanistayDetailedSearchRequest, - DanistayDocumentMarkdown, CompactDanistaySearchResult -) -from emsal_mcp_module.client import EmsalApiClient -from emsal_mcp_module.models import ( - EmsalSearchRequest, EmsalDocumentMarkdown, CompactEmsalSearchResult -) -from uyusmazlik_mcp_module.client import UyusmazlikApiClient -from uyusmazlik_mcp_module.models import ( - UyusmazlikSearchRequest, UyusmazlikSearchResponse, UyusmazlikDocumentMarkdown, - UyusmazlikBolumEnum, UyusmazlikTuruEnum, UyusmazlikKararSonucuEnum -) -from anayasa_mcp_module.client import AnayasaMahkemesiApiClient -from anayasa_mcp_module.bireysel_client import AnayasaBireyselBasvuruApiClient -from anayasa_mcp_module.unified_client import AnayasaUnifiedClient -from anayasa_mcp_module.models import ( - AnayasaNormDenetimiSearchRequest, - AnayasaSearchResult, - AnayasaDocumentMarkdown, - AnayasaBireyselReportSearchRequest, - AnayasaBireyselReportSearchResult, - AnayasaBireyselBasvuruDocumentMarkdown, - AnayasaUnifiedSearchRequest, - AnayasaUnifiedSearchResult, - AnayasaUnifiedDocumentMarkdown, - # Removed enum imports - now using Literal strings in models -) -# KIK Module Imports -from kik_mcp_module.client import KikApiClient -from kik_mcp_module.models import ( - KikKararTipi, - KikSearchRequest, - KikSearchResult, - KikDocumentMarkdown -) - -from rekabet_mcp_module.client import RekabetKurumuApiClient -from rekabet_mcp_module.models import ( - RekabetKurumuSearchRequest, - RekabetSearchResult, - RekabetDocument, - RekabetKararTuruGuidEnum -) - -from sayistay_mcp_module.client import SayistayApiClient -from sayistay_mcp_module.models import ( - GenelKurulSearchRequest, GenelKurulSearchResponse, - TemyizKuruluSearchRequest, TemyizKuruluSearchResponse, - DaireSearchRequest, DaireSearchResponse, - SayistayDocumentMarkdown, - SayistayUnifiedSearchRequest, SayistayUnifiedSearchResult, - SayistayUnifiedDocumentMarkdown -) -from sayistay_mcp_module.enums import DaireEnum, KamuIdaresiTuruEnum, WebKararKonusuEnum -from sayistay_mcp_module.unified_client import SayistayUnifiedClient - -# KVKK Module Imports -from kvkk_mcp_module.client import KvkkApiClient -from kvkk_mcp_module.models import ( - KvkkSearchRequest, - KvkkSearchResult, - KvkkDocumentMarkdown -) - -# BDDK Module Imports -from bddk_mcp_module.client import BddkApiClient -from bddk_mcp_module.models import ( - BddkSearchRequest, - BddkSearchResult, - BddkDocumentMarkdown -) - - -# Create a placeholder app that will be properly initialized after tools are defined -from fastmcp import FastMCP - -# Placeholder app for decorators - will be replaced in create_app() after all tools are defined -app = FastMCP("Yargı MCP Server Placeholder") - -# --- Tool Documentation Resources --- -@app.resource("docs://tools/yargitay") -async def get_yargitay_tools_documentation() -> str: - """Get document content as Markdown.""" - return """ -# Yargıtay (Court of Cassation) Tools Documentation - -## Court Hierarchy and Position -Yargıtay is Turkey's highest civil and criminal court. It serves as the final appellate authority and establishes legal precedents for civil and criminal cases. - -**Dual API System:** -- **Primary API (search_yargitay_detailed)**: Official karararama.yargitay.gov.tr -- **Bedesten API (search_bedesten_unified)**: Unified access to bedesten.adalet.gov.tr (see docs://tools/bedesten_unified) - -## Chamber Filtering Options (52 Total) - -### Civil Chambers (Hukuk Daireleri) -- **Civil General Assembly** (Hukuk Genel Kurulu) -- **1st Civil Chamber** through **23rd Civil Chamber** (23 civil chambers) -- **Civil Chambers Presidents Board** (Hukuk Daireleri Başkanlar Kurulu) - -### Criminal Chambers (Ceza Daireleri) -- **Criminal General Assembly** (Ceza Genel Kurulu) -- **1st Criminal Chamber** through **23rd Criminal Chamber** (23 criminal chambers) -- **Criminal Chambers Presidents Board** (Ceza Daireleri Başkanlar Kurulu) - -### General Assemblies -- **Grand General Assembly** (Büyük Genel Kurulu) - -## Search Techniques - -### Primary API (search_yargitay_detailed) -``` -Simple search: "mülkiyet" -AND operator: "mülkiyet AND tapu" -OR operator: "mülkiyet OR tapu" -NOT operator: "mülkiyet NOT satış" -Wildcard: "mülk*" -Exact phrase: "\"mülkiyet hakkı\"" -``` - -### Bedesten API (search_bedesten_unified) -For detailed usage, see docs://tools/bedesten_unified -``` -Regular search: phrase="mülkiyet kararı", court_types=["YARGITAYKARARI"] -Exact phrase: phrase="\"mülkiyet kararı\"", court_types=["YARGITAYKARARI"] -Date filtering: kararTarihiStart="2024-01-01T00:00:00.000Z" -``` - -## Usage Scenarios -- **Precedent research**: Supreme court decisions on specific topics -- **Chamber-specific search**: Relevant chambers for specific legal areas -- **Historical analysis**: Decision trends in specific periods -- **Jurisprudence tracking**: Changes in legal opinions - -## Best Practices -1. **Use dual APIs**: Try both APIs for maximum coverage -2. **Chamber filtering**: Select chambers based on relevant legal area -3. **Exact phrases**: Use "\"term\"" for precise terms in Bedesten API -4. **Date range**: Focus on last 2-3 years for recent developments - -## Common Civil Chambers -- **1st Civil**: Property, land registry, liens -- **4th Civil**: Labor law, collective agreements -- **11th Civil**: Insurance, social security -- **15th Civil**: Compensation, tort -- **21st Civil**: Execution and bankruptcy - -## Common Criminal Chambers -- **1st Criminal**: General criminal offenses -- **8th Criminal**: Economic and commercial crimes -- **12th Criminal**: Official misconduct -""" - -@app.resource("docs://tools/danistay") -async def get_danistay_tools_documentation() -> str: - """Get document content as Markdown.""" - return """ -# Danıştay (Council of State) Tools Documentation - -## Court Hierarchy and Position -Danıştay is Turkey's highest administrative court. It makes final decisions on administrative acts and actions. - -**Triple API System:** -- **Keyword API (search_danistay_by_keyword)**: AND/OR/NOT logic -- **Detailed API (search_danistay_detailed)**: Comprehensive criteria -- **Bedesten API (search_bedesten_unified)**: Unified access (see docs://tools/bedesten_unified) - -## Chamber Filtering Options (27 Total) - -### Main Councils -- **Grand General Assembly** (Büyük Gen.Kur.) -- **Administrative Cases Council** (İdare Dava Daireleri Kurulu) -- **Tax Cases Council** (Vergi Dava Daireleri Kurulu) -- **Precedents Unification Council** (İçtihatları Birleştirme Kurulu) - -### Chambers (1-17) -- **1st Chamber** through **17th Chamber** (Administrative case chambers) - -### Military Courts -- **Military High Administrative Court** (Askeri Yüksek İdare Mahkemesi) -- **Military High Administrative Court 1st-3rd Chambers** - -## Search Techniques - -### Keyword API -``` -AND logic: andKelimeler=["imar", "plan"] -OR logic: orKelimeler=["iptal", "yürütmeyi durdurma"] -NOT logic: notKelimeler=["ceza"] -``` - -### Detailed API -``` -Chamber selection: daire="3. Daire" -Case year: esasYil="2024" -Decision date: kararTarihiBaslangic="01.01.2024" -Legislation: mevzuatId=123 -``` - -### Bedesten API -``` -Regular: phrase="idari işlem" -Exact: phrase="\"idari işlem\"" -Date: kararTarihiStart="2024-01-01T00:00:00.000Z" -``` - -## Usage Scenarios -- **Administrative law research**: Public administration decisions -- **Tax law**: Financial matters and tax disputes -- **Urban planning law**: City planning and building permits -- **Personnel law**: Civil servant rights - -## Common Chamber Specializations -- **1st Chamber**: Municipal, urban planning, environment -- **2nd Chamber**: Tax, customs, financial -- **3rd Chamber**: Personnel, personal rights -- **5th Chamber**: Administrative fines -- **8th Chamber**: Higher education, education -- **10th Chamber**: Health, social security - -## Best Practices -1. **Triple API**: Use all three APIs for maximum coverage -2. **Chamber selection**: Choose specialized chambers by subject area -3. **Mevzuat bağlantısı**: İlgili kanun/tüzükle filtreleme -4. **Kesin terim**: İdari hukuk terminolojisi için exact search -""" - -@app.resource("docs://tools/constitutional_court") -async def get_constitutional_court_tools_documentation() -> str: - """Get document content as Markdown.""" - return """ -# Anayasa Mahkemesi (Constitutional Court) Tools Documentation - -## Court Position -Constitutional Court is Turkey's highest judicial body. It has two main functions: - -### 1. Norm Control (Norm Control) -**Tool**: search_anayasa_norm_denetimi_decisions -- Reviews constitutional compliance of laws and regulations -- Abstract and concrete norm control - -### 2. Individual Application (Individual Application) -**Tool**: search_anayasa_bireysel_basvuru_report -- Citizens' fundamental rights violation applications -- Turkey's human rights protection mechanism - -## Norm Control Features - -### Comprehensive Filtering -- **Application type**: Annulment, Objection, Other -- **Applicant**: President, Parliament, Courts -- **Legislation type**: Law, Decree, Regulation, Rules of procedure -- **Result type**: Annulment, Rejection, Partial annulment - -### Advanced Search -- **Member names**: Full names of participating justices -- **Rapporteur**: Case rapporteur -- **Dissenting opinion**: Minority opinion, different view -- **Press release**: Important decisions - -## Bireysel Başvuru Özellikleri - -### Temel Haklar Kategorileri -- **Yaşam hakkı**: Ölüm olayları, güvenlik -- **Adil yargılanma**: Süre, tarafsızlık, duruşma hakkı -- **İfade özgürlüğü**: Basın, düşünce, akademik özgürlük -- **Din özgürlüğü**: İbadet, vicdan özgürlüğü -- **Mülkiyet hakkı**: Kamulaştırma, tapu -- **Özel hayat**: Gizlilik, aile hayatı - -### Başvuru Süreci -- **Yurtiçi yollar**: Önce mahkeme kararı gerekli -- **Süre sınırı**: 30 gün (60 gün istisnai) -- **Kabul edilebilirlik**: Ön inceleme kriterleri - -## Paginated Content (5,000 characters) -Her iki tool da sayfalanmış Markdown döndürür: -- **page_number**: Sayfa numarası (1'den başlar) -- **total_pages**: Toplam sayfa sayısı -- **current_page**: Mevcut sayfa - -## Usage Scenarios - -### Norm Denetimi -- **Kanun anayasaya uygunluk**: Yeni çıkan kanunların kontrolü -- **Mahkeme iptali**: Kanunun belirli maddeleri -- **Mevzuat uyum**: Anayasa değişikliği sonrası - -### Bireysel Başvuru -- **İnsan hakları araştırması**: AİHM öncesi iç hukuk -- **Temel hak ihlalleri**: Sistematik ihlal tespiti -- **Emsal karar**: Benzer davalar için içtihat - -## Parameter Details -### search_anayasa_norm_denetimi_decisions -- **keywords_all**: Keywords for AND logic (all must be present) -- **keywords_any**: Keywords for OR logic (any can be present) -- **keywords_exclude**: Keywords to exclude from results -- **period**: Constitutional period - "ALL", "1" (1961 Constitution), "2" (1982 Constitution) -- **case_number_esas**: Case registry number (e.g., '2023/123') -- **decision_number_karar**: Decision number (e.g., '2023/456') -- **first_review_date_start/end**: First review date range (DD/MM/YYYY) -- **decision_date_start/end**: Decision date range (DD/MM/YYYY) -- **application_type**: "ALL", "1" (İptal), "2" (İtiraz), "3" (Diğer) -- **applicant_general_name**: General applicant name -- **applicant_specific_name**: Specific applicant name -- **official_gazette_date_start/end**: Official Gazette date range -- **official_gazette_number_start/end**: Official Gazette number range -- **has_press_release**: "ALL", "0" (No), "1" (Yes) -- **has_dissenting_opinion**: "ALL", "0" (No), "1" (Yes) -- **has_different_reasoning**: "ALL", "0" (No), "1" (Yes) -- **attending_members_names**: List of attending members' exact names -- **rapporteur_name**: Rapporteur's exact name -- **norm_type**: Type of reviewed norm (law, decree, regulation, etc.) -- **norm_id_or_name**: Number or name of the norm -- **norm_article**: Article number of the norm -- **review_outcomes**: List of review outcomes -- **reason_for_final_outcome**: Main reason for decision outcome -- **basis_constitution_article_numbers**: Supporting Constitution article numbers -- **results_per_page**: Results per page (10, 20, 30, 40, 50) -- **page_to_fetch**: Page number to fetch -- **sort_by_criteria**: Sort criteria ('KararTarihi', 'YayinTarihi', 'Toplam') - -### search_anayasa_bireysel_basvuru_report -- **keywords**: Keywords for AND logic (all must be present) -- **page_to_fetch**: Page number for the report (default: 1) - -### Document Tools -- **document_url**: URL path or full URL of the decision -- **page_number**: Page number for paginated content (1-indexed, default: 1) - -## Best Practices -1. **Norm control önce**: Kanun iptal edilmiş mi kontrol -2. **Bireysel başvuru ikinci**: Kişisel hak ihlalleri için -3. **Tarih aralığı**: Anayasa değişiklikleri sonrası dönemler -4. **Anahtar kelime kombinasyonu**: Temel hak + konu alanı -5. **Sayfa yönetimi**: Uzun kararlarda sayfa sayfa okuyun -""" - -@app.resource("docs://tools/emsal") -async def get_emsal_tools_documentation() -> str: - """Get document content as Markdown.""" - return """ -# Emsal (UYAP Precedent System) Tools Documentation - -## System Position -Central precedent decision system providing access to all court decisions through the UYAP system. - -## Court Options -- **Yargıtay**: First and second instance courts -- **Danıştay**: Administrative court decisions -- **Other**: Regional courts of justice, civil courts - -## Advanced Filtering Features -- **Court type**: Civil, criminal, administrative -- **Case/Decision number**: File tracking system -- **Date range**: Flexible date selection -- **Content search**: Keyword search within decision text - -## Usage Scenarios -- **Kapsamlı emsal**: Tüm mahkeme seviyelerinden karar toplama -- **Güncel içtihat**: En son hukuki gelişmeler -- **Cross-reference**: Farklı mahkeme görüşlerini karşılaştırma - -## Best Practices -1. **Spesifik terimler**: Hukuki terminoloji kullanın -2. **Geniş arama**: Önce genel, sonra spesifik -3. **Tarih stratejisi**: Mevzuat değişiklikleri dikkate alın -4. **Cross-platform**: Aynı konuyu farklı mahkemelerde arayın -""" - -@app.resource("docs://tools/uyusmazlik") -async def get_uyusmazlik_tools_documentation() -> str: - """Get document content as Markdown.""" - return """ -# Uyuşmazlık Mahkemesi Tools Documentation - -## Court Position -Adli ve idari yargı arasındaki görev uyuşmazlıklarını çözen özel yetkili mahkeme. - -## Dispute Types -- **Görev uyuşmazlığı**: Hangi mahkeme bakacak konusunda anlaşmazlık -- **Hüküm uyuşmazlığı**: Çelişkili mahkeme kararları -- **Yetki uyuşmazlığı**: Yerel yetki sorunları - -## Form-Based Search Criteria -- **Karar türü**: Müspet, menfi, hüküm uyuşmazlığı -- **Taraf mahkemeler**: Adli-idari yargı organları -- **Konu alanı**: Hukuk dalı bazlı filtreleme -- **Tarih aralığı**: Karar tarihi seçimi - -## Usage Scenarios -- **Yargı türü belirleme**: Hangi mahkemenin yetkili olduğu -- **Çelişkili kararlar**: Farklı mahkeme kararları arasındaki uyuşmazlık -- **Yetki sorunları**: Mahkeme yetkisi tartışmaları - -## Best Practices -1. **Net kriterler**: Arama kriterlerini spesifik tutun -2. **Taraf bilgisi**: Uyuşmazlık taraflarını belirtin -3. **Konu odaklı**: İlgili hukuk dalını seçin -""" - -@app.resource("docs://tools/kik") -async def get_kik_tools_documentation() -> str: - """Get document content as Markdown.""" - return """ -# KİK (Kamu İhale Kurumu) Tools Documentation - -## Kurum Konumu -Kamu ihale uyuşmazlıklarının ilk ve son merci çözüm organı. Kamu İhale Kanunu kapsamındaki tüm ihaleler için yetkili. - -## Decision Types -- **Uyuşmazlık**: İhale süreç itirazları -- **Düzenleyici**: Mevzuat ve uygulama kararları -- **Mahkeme**: Mahkeme kararlarının uygulanması - -## Filtreleme Seçenekleri -- **Karar numarası**: 2024/UH.II-1766 formatında -- **Tarih aralığı**: Karar tarihi filtreleme -- **İhaleyi yapan idare**: Bakanlık, belediye, hastane, üniversite -- **Başvuru sahibi**: Şirket, firma adı -- **İhale konusu**: Mal, hizmet, yapım işi - -## Sayfalanmış İçerik Özelliği -5.000 karakterlik sayfalar halinde Markdown formatında sunulur. - -## Usage Scenarios -- **İhale hukuku**: Kamu alımları, süreç kuralları -- **Başvuru hazırlığı**: Benzer davalar, emsal kararlar -- **Mevzuat yorumu**: Kamu İhale Kanunu uygulaması -- **İtiraz stratejisi**: Başarılı itiraz örnekleri - -## İhale Süreç Aşamaları -1. **İhale öncesi**: İlan, şartname hazırlığı -2. **İhale aşaması**: Teklif verme, değerlendirme -3. **İhale sonrası**: Sonuç bildirimi, itirazlar -4. **Sözleşme**: İmza, uygulama - -## Parameter Details -### search_kik_decisions -- **karar_tipi**: Decision type - "rbUyusmazlik" (disputes), "rbDuzenleyici" (regulatory), "rbMahkeme" (court) -- **karar_no**: Decision number (e.g., '2024/UH.II-1766') -- **karar_tarihi_baslangic**: Decision start date (DD.MM.YYYY format) -- **karar_tarihi_bitis**: Decision end date (DD.MM.YYYY format) -- **basvuru_sahibi**: Applicant name/company -- **ihaleyi_yapan_idare**: Procuring entity (ministry, municipality, etc.) -- **basvuru_konusu_ihale**: Tender subject/description -- **karar_metni**: Text search with operators: +word (AND), -word (exclude) -- **yil**: Decision year -- **resmi_gazete_tarihi**: Official Gazette date (DD.MM.YYYY) -- **resmi_gazete_sayisi**: Official Gazette number -- **page**: Results page number - -### get_kik_document_markdown -- **karar_id**: Base64 encoded decision identifier from search results -- **page_number**: Page number for paginated content (1-indexed, default: 1) - -## Best Practices -1. **İhale türü**: Açık, belli istekliler arası, pazarlık -2. **Süreç aşaması**: Hangi aşamada sorun olduğu -3. **Hukuki dayanak**: İlgili KİK kanun maddesi -4. **Sayfa yönetimi**: Uzun kararları bölümler halinde okuyun -""" - -@app.resource("docs://tools/rekabet") -async def get_rekabet_tools_documentation() -> str: - """Get document content as Markdown.""" - return """ -# Rekabet Kurumu (Competition Authority) Tools Documentation - -## Kurum Konumu -Rekabet hukuku ihlallerini inceleyen ve ceza veren idari otorite. Rekabet Kanunu kapsamında yetkili. - -## Decision Types -- **Birleşme ve Devralma**: Şirket satın almaları, füzyonlar -- **Rekabet İhlali**: Anlaşma, hakim durum kötüye kullanımı -- **Menfi Tespit ve Muafiyet**: İhlal yok kararları, muafiyetler -- **Özelleştirme**: Kamu şirketleri satışı onayları - -## Filtreleme Özellikleri -- **PDF metin arama**: Tam metin içinde kelime arama -- **Karar türü**: Spesifik kategori seçimi -- **Tarih aralığı**: 1997'den günümüze karar arşivi -- **Sektör**: Telekomünikasyon, bankacılık, enerji, perakende - -## Rekabet Hukuku Temel Kavramları -- **Hakim durum**: Pazar gücü -- **Kartel**: Fiyat anlaşması -- **Dikey anlaşmalar**: Tedarikci-bayi ilişkileri -- **Konsantrasyon**: Birleşme işlemleri - -## Usage Scenarios -- **Antitrust araştırması**: Tekelleşme, kartel soruşturmaları -- **Birleşme incelemesi**: M&A transaction değerlendirmesi -- **Sektör analizi**: Belirli pazarlardaki rekabet durumu -- **Ceza hesaplama**: İhlal cezası örnekleri - -## Sektörel Uzmanlık Alanları -1. **Telekomünikasyon**: Operatör rekabeti -2. **Enerji**: Elektrik, doğalgaz piyasası -3. **Finans**: Bankacılık, sigorta -4. **Perakende**: Zincir mağazalar -5. **İnşaat**: Müteahhitlik sektörü - -## Parameter Details -### search_rekabet_kurumu_decisions -- **sayfaAdi**: Search in decision title (Başlık) -- **YayinlanmaTarihi**: Publication date (DD.MM.YYYY format) -- **PdfText**: Search text. For exact phrases use double quotes: "vertical agreement" -- **KararTuru**: Decision type - "Birleşme ve Devralma", "Rekabet İhlali", etc. -- **KararSayisi**: Decision number (Karar Sayısı) -- **KararTarihi**: Decision date (DD.MM.YYYY format) -- **page**: Page number for results list - -### get_rekabet_kurumu_document -- **karar_id**: GUID from search results -- **page_number**: Requested page for Markdown content (1-indexed, default: 1) - -## Best Practices -1. **Sektör odaklı**: İlgili sektörde arama yapın -2. **Karar türü seçimi**: İhtiyacınıza uygun kategori -3. **Güncel mevzuat**: Mevzuat değişiklikleri takibi -4. **Sayfa yönetimi**: Uzun analizleri bölümler halinde -""" - -@app.resource("docs://tools/bedesten_unified") -async def get_bedesten_unified_documentation() -> str: - """Get document content as Markdown.""" - return """ -# Bedesten API Mahkemeleri Tools Documentation - -## Bedesten API Sistemi -bedesten.adalet.gov.tr üzerinden Türk adalet sistemi hiyerarşisindeki mahkemelere erişim. - -## Mahkeme Hiyerarşisi Kapsamı - -### 1. Yerel Hukuk Mahkemeleri (Local Civil Courts) -**Tool**: search_yerel_hukuk_bedesten -- **Konum**: İlk derece mahkemeler -- **Yetki**: Hukuki uyuşmazlıklar (sözleşme, tazminat, mülkiyet) -- **Önem**: Toplumun günlük hukuki sorunları - -### 2. İstinaf Hukuk Mahkemeleri (Civil Courts of Appeals) -**Tool**: search_istinaf_hukuk_bedesten -- **Konum**: Orta derece (Yerel -> İstinaf -> Yargıtay) -- **Yetki**: Yerel mahkeme kararlarına itiraz -- **Önem**: Temyiz öncesi son kontrol - -### 3. Kanun Yararına Bozma (KYB) -**Tool**: search_kyb_bedesten -- **Konum**: Olağanüstü kanun yolu -- **Başvuru sahibi**: Cumhuriyet Başsavcılığı -- **Amaç**: Hukuka aykırı kararları düzeltme -- **Özellik**: Sanık aleyhine olsa bile hukuk yararına - -## Ortak Bedesten API Özellikleri - -### Tarih Filtreleme (ISO 8601) -``` -Başlangıç: kararTarihiStart="2024-01-01T00:00:00.000Z" -Bitiş: kararTarihiEnd="2024-12-31T23:59:59.999Z" -Tek gün: "2024-06-25T00:00:00.000Z" - "2024-06-25T23:59:59.999Z" -``` - -### Kesin Cümle Arama -``` -Normal: phrase="sözleşme ihlali" (kelimeler ayrı ayrı) -Kesin: phrase="\"sözleşme ihlali\"" (tam cümle) -``` - -### Sayfalama -- **pageSize**: 1-100 arası sonuç sayısı -- **pageNumber**: Sayfa numarası (1'den başlar) - -## Mahkeme Özellikleri - -### Yerel Hukuk Mahkemeleri -**Yaygın Dava Türleri**: -- Sözleşme ihlali davaları -- Tazminat talepleri -- Mülkiyet uyuşmazlıkları -- Aile hukuku (boşanma, nafaka) -- Ticari uyuşmazlıklar (küçük-orta ölçek) - -**Kullanım Senaryoları**: -- Günlük hukuki sorunlar -- Vatandaş hakları -- Ticaret hukuku temelleri -- İcra takipleri - -### İstinaf Hukuk Mahkemeleri -**İnceleme Kapsamı**: -- Yerel mahkeme kararlarının kontrolü -- Hukuki ve maddi hata arayışı -- Yeniden yargılama (sınırlı) - -**Kullanım Senaryoları**: -- Temyiz stratejisi gelişitirme -- İstinaf mahkemesi içtihatları -- Yerel-üst mahkeme uyumu analizi - -### Kanun Yararına Bozma (KYB) -**Başvuru Koşulları**: -- Kesinleşmiş mahkeme kararı -- Hukuka açık aykırılık -- Cumhuriyet Başsavcılığı başvurusu -- Sanık aleyhine sonuç doğurmama - -**Kullanım Senaryoları**: -- Sistematik hukuki hatalar -- İçtihat birliğini sağlama -- Hukuk güvenliği -- Nadir ve özel hukuki durumlar - -## Arama Stratejileri - -### Hiyerarşik Arama -``` -1. Yerel mahkeme -> Gündelik sorunlar -2. İstinaf -> Kompleks yorumlar -3. KYB -> İstisnai hukuki durumlar -``` - -### Kesin Terim Kullanımı -``` -Yerel: "\"sözleşme ihlali\"" -İstinaf: "\"temyiz incelemesi\"" -KYB: "\"kanun yararına bozma\"" -``` - -### Tarih Stratejisi -- **Son 2 yıl**: Güncel içtihat -- **5-10 yıl**: Yerleşik görüşler -- **Mevzuat değişikliği sonrası**: Yeni uygulamalar - -## Best Practices -1. **Hiyerarşi takibi**: Alt mahkemeden üst mahkemeye -2. **Kesin cümle**: Hukuki terimler için "\"terim\"" -3. **Tarih aralığı**: İlgili mevzuat dönemleri -4. **Cross-reference**: Aynı konuyu farklı seviyelerde -5. **Minimal sonuç**: KYB çok nadir, az sonuç beklenir - -## Document ID Formatı -Tüm Bedesten mahkemeleri documentId döndürür: -- **Format**: Alfanumerik string -- **Kullanım**: get_*_bedesten_document_markdown fonksiyonları -- **İçerik**: HTML/PDF -> Markdown conversion -""" - -@app.resource("docs://tools/sayistay") -async def get_sayistay_tools_documentation() -> str: - """Get document content as Markdown.""" - return """ -# Sayıştay (Court of Accounts) Tools Documentation - -## Sayıştay'ın Konumu -Türkiye'nin en üst mali denetim organı. Kamu kaynaklarının kullanımını denetler ve mali disiplini sağlar. - -## Üç Tür Karar Sistemi - -### 1. Genel Kurul Kararları (Interpretive Rulings) -**Tool**: search_sayistay_genel_kurul -- **İşlev**: Mali mevzuat yorumlama -- **Kapsam**: 2006-2024 yılları arası -- **Özellik**: Bağlayıcı yorumlar - -**Filtreleme Seçenekleri**: -- **Karar numarası**: Spesifik karar arama -- **Tarih aralığı**: Başlangıç-bitiş tarihleri -- **Karar tamamı**: Tam metin arama (400 karakter) - -### 2. Temyiz Kurulu Kararları (Appeals Board) -**Tool**: search_sayistay_temyiz_kurulu -- **İşlev**: Daire kararlarına itiraz incelemesi -- **8 Daire Filtreleme**: Uzmanlık alanlarına göre - -**Daire Uzmanlaşmaları**: -- **1. Daire**: Genel bütçeli idareler -- **2. Daire**: Mahalli idareler -- **3. Daire**: Sosyal güvenlik kurumları -- **4. Daire**: KİT ve bağlı ortaklıklar -- **5. Daire**: Düzenleyici kuruluşlar -- **6. Daire**: Vakıflar, dernekler -- **7. Daire**: Üniversiteler, eğitim -- **8. Daire**: Yatırım projeleri - -**Filtreleme Seçenekleri**: -- **İdare türü**: Bakanlık, belediye, üniversite, KİT -- **Temyiz karar**: Tam metin arama -- **Konu sınıflandırması**: Harcama, gelir, taşınır-taşınmaz - -### 3. Daire Kararları (Chamber Decisions) -**Tool**: search_sayistay_daire -- **İşlev**: İlk derece denetim bulguları -- **8 Daire**: Aynı uzmanlaşma alanları - -**Filtreleme Seçenekleri**: -- **Yargılama dairesi**: 1-8 arası daire seçimi -- **Hesap yılı**: Mali yıl bazlı -- **Web karar metni**: İçerik arama - -## Ortak Özellikler - -### Sayfalanmış Markdown -Tüm Sayıştay belgeleri sayfalanmış format: -- **5.000 karakter** per sayfa -- **page_number**: Sayfa numarası -- **total_pages**: Toplam sayfa - -### Tarih Aralığı Desteği -- **Genel Kurul**: 2006-2024 (18 yıl) -- **Temyiz/Daire**: Mevcut veriler üzerinde - -## Usage Scenarios - -### Mali Mevzuat Araştırması -``` -Genel Kurul -> Hukuki yorum -Temyiz -> Uygulama detayları -Daire -> Spesifik örnekler -``` - -### Kamu Mali Yönetimi -- **Bütçe uygulama**: Harcama usulleri -- **İhale süreçleri**: Kamu alımları denetimi -- **Personel giderleri**: Özlük hakları mali boyutu -- **Yatırım projeleri**: Büyük ölçekli projeler - -### Kurumsal Denetim -- **KİT yönetimi**: Kamu iktisadi teşebbüsleri -- **Belediye maliyesi**: Yerel yönetim harcamaları -- **Üniversite bütçesi**: Yükseköğretim mali yönetimi -- **Sosyal güvenlik**: SGK, Bağ-Kur mali işlemleri - -## Arama Stratejileri - -### Hiyerarşik Yaklaşım -1. **Genel Kurul**: Konunun hukuki çerçevesi -2. **Temyiz**: Tartışmalı uygulamalar -3. **Daire**: Günlük uygulama örnekleri - -### Daire Bazlı Strateji -``` -Mali konu -> İlgili daire seçimi -> Derinlemesine arama -Örnek: KİT mali sorunları -> 4. Daire -``` - -### Tarih Odaklı Strateji -- **Son 2 yıl**: Güncel uygulamalar -- **5 yıl**: Yerleşik görüşler -- **2006-2024**: Tarihsel gelişim - -## Best Practices -1. **Daire uzmanlaşması**: İlgili kuruma uygun daire -2. **Hiyerarşik sıralama**: Genel Kurul -> Temyiz -> Daire -3. **Mali dönem**: Bütçe yılları bazında arama -4. **Teknik terimler**: Mali mevzuat terminolojisi -5. **Cross-reference**: Farklı seviyelerden görüş karşılaştırma - -## İdare Türü Kodları -- **1**: Genel bütçeli -- **2**: Özel bütçeli -- **3**: Düzenleyici kuruluşlar -- **4**: Mahalli idareler -- **5**: Sosyal güvenlik -- **6**: KİT -- **7**: Vakıf/dernek -- **8**: Diğer kamu kuruluşları -""" - -@app.resource("docs://tools/kvkk") -async def get_kvkk_tools_documentation() -> str: - """Get document content as Markdown.""" - return """ -# KVKK (Personal Data Protection Authority) Tools - -## Overview -KVKK (Kişisel Verilerin Korunması Kurulu) - Turkey's GDPR equivalent authority. - -## Search Features -- **Brave Search API**: Searches kvkk.gov.tr with Turkish terms -- **Site-targeted**: Auto `site:kvkk.gov.tr "karar özeti"` -- **Pagination**: page and pageSize parameters -- **5,000-char pages**: Paginated Markdown documents - -## Common Search Terms -**Violations**: "veri ihlali", "açık rıza", "idari para cezası" -**Compliance**: "GDPR uyum", "veri koruma", "güvenlik tedbirleri" -**Sectors**: "e-ticaret", "bankacılık", "sağlık", "mobil uygulama" - -## Key Decision Types -- **Fines**: Data breaches, consent violations -- **Compliance**: GDPR alignment, corporate policies -- **Breach notifications**: 24-hour rule violations - -## Usage Tips -1. Use Turkish legal terms for best results -2. Combine sector + violation type searches -3. Use page_number for long decisions -4. Focus on recent 2-3 years for current practices -""" - - -# --- API Client Instances --- -yargitay_client_instance = YargitayOfficialApiClient() -danistay_client_instance = DanistayApiClient() -emsal_client_instance = EmsalApiClient() -uyusmazlik_client_instance = UyusmazlikApiClient() -anayasa_norm_client_instance = AnayasaMahkemesiApiClient() -anayasa_bireysel_client_instance = AnayasaBireyselBasvuruApiClient() -anayasa_unified_client_instance = AnayasaUnifiedClient() -kik_client_instance = KikApiClient() -rekabet_client_instance = RekabetKurumuApiClient() -bedesten_client_instance = BedestenApiClient() -sayistay_client_instance = SayistayApiClient() -sayistay_unified_client_instance = SayistayUnifiedClient() -kvkk_client_instance = KvkkApiClient() -bddk_client_instance = BddkApiClient() - - -KARAR_TURU_ADI_TO_GUID_ENUM_MAP = { - "": RekabetKararTuruGuidEnum.TUMU, # Keep for backward compatibility - "ALL": RekabetKararTuruGuidEnum.TUMU, # Map "ALL" to TUMU - "Birleşme ve Devralma": RekabetKararTuruGuidEnum.BIRLESME_DEVRALMA, - "Diğer": RekabetKararTuruGuidEnum.DIGER, - "Menfi Tespit ve Muafiyet": RekabetKararTuruGuidEnum.MENFI_TESPIT_MUAFIYET, - "Özelleştirme": RekabetKararTuruGuidEnum.OZELLESTIRME, - "Rekabet İhlali": RekabetKararTuruGuidEnum.REKABET_IHLALI, -} - -# --- MCP Tools for Yargitay --- -""" -@app.tool( - description="Search Yargıtay decisions with 52 chamber filtering and advanced operators", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -async def search_yargitay_detailed( - arananKelime: str = Field("", description="Turkish search keyword. Supports +required -excluded \"exact phrase\" operators"), - birimYrgKurulDaire: str = Field("ALL", description="Chamber selection (52 options: Civil/Criminal chambers, General Assemblies)"), - esasYil: str = Field("", description="Case year for 'Esas No'."), - esasIlkSiraNo: str = Field("", description="Starting sequence number for 'Esas No'."), - esasSonSiraNo: str = Field("", description="Ending sequence number for 'Esas No'."), - kararYil: str = Field("", description="Decision year for 'Karar No'."), - kararIlkSiraNo: str = Field("", description="Starting sequence number for 'Karar No'."), - kararSonSiraNo: str = Field("", description="Ending sequence number for 'Karar No'."), - baslangicTarihi: str = Field("", description="Start date for decision search (DD.MM.YYYY)."), - bitisTarihi: str = Field("", description="End date for decision search (DD.MM.YYYY)."), - # pageSize: int = Field(10, ge=1, le=10, description="Number of results per page."), - pageNumber: int = Field(1, ge=1, description="Page number to retrieve.") -) -> CompactYargitaySearchResult: - # Search Yargıtay decisions using primary API with 52 chamber filtering and advanced operators. - - # Convert "ALL" to empty string for API compatibility - if birimYrgKurulDaire == "ALL": - birimYrgKurulDaire = "" - - pageSize = 10 # Default value - - search_query = YargitayDetailedSearchRequest( - arananKelime=arananKelime, - birimYrgKurulDaire=birimYrgKurulDaire, - esasYil=esasYil, - esasIlkSiraNo=esasIlkSiraNo, - esasSonSiraNo=esasSonSiraNo, - kararYil=kararYil, - kararIlkSiraNo=kararIlkSiraNo, - kararSonSiraNo=kararSonSiraNo, - baslangicTarihi=baslangicTarihi, - bitisTarihi=bitisTarihi, - siralama="3", - siralamaDirection="desc", - pageSize=pageSize, - pageNumber=pageNumber - ) - - logger.info(f"Tool 'search_yargitay_detailed' called: {search_query.model_dump_json(exclude_none=True, indent=2)}") - try: - api_response = await yargitay_client_instance.search_detailed_decisions(search_query) - if api_response and api_response.data and api_response.data.data: - # Convert to clean decision entries without arananKelime field - clean_decisions = [ - CleanYargitayDecisionEntry( - id=decision.id, - daire=decision.daire, - esasNo=decision.esasNo, - kararNo=decision.kararNo, - kararTarihi=decision.kararTarihi, - document_url=decision.document_url - ) - for decision in api_response.data.data - ] - return CompactYargitaySearchResult( - decisions=clean_decisions, - total_records=api_response.data.recordsTotal if api_response.data else 0, - requested_page=search_query.pageNumber, - page_size=search_query.pageSize) - 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) - except Exception as e: - logger.exception(f"Error in tool 'search_yargitay_detailed'.") - raise - -@app.tool( - description="Get Yargıtay decision text in Markdown format", - annotations={ - "readOnlyHint": True, - "idempotentHint": True - } -) -async def get_yargitay_document_markdown(id: str) -> YargitayDocumentMarkdown: - # Get Yargıtay decision text as Markdown. Use ID from search results. - logger.info(f"Tool 'get_yargitay_document_markdown' called for ID: {id}") - if not id or not id.strip(): raise ValueError("Document ID must be a non-empty string.") - try: - return await yargitay_client_instance.get_decision_document_as_markdown(id) - except Exception as e: - logger.exception(f"Error in tool 'get_yargitay_document_markdown'.") - raise -""" - -# --- MCP Tools for Danistay --- -""" -@app.tool( - description="Search Danıştay decisions with keyword logic (AND/OR/NOT operators)", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -async def search_danistay_by_keyword( - andKelimeler: List[str] = Field(default_factory=list, description="Keywords for AND logic, e.g., ['word1', 'word2']"), - orKelimeler: List[str] = Field(default_factory=list, description="Keywords for OR logic."), - notAndKelimeler: List[str] = Field(default_factory=list, description="Keywords for NOT AND logic."), - notOrKelimeler: List[str] = Field(default_factory=list, description="Keywords for NOT OR logic."), - pageNumber: int = Field(1, ge=1, description="Page number."), - # pageSize: int = Field(10, ge=1, le=10, description="Results per page.") -) -> CompactDanistaySearchResult: - # Search Danıştay decisions with keyword logic. - - pageSize = 10 # Default value - - search_query = DanistayKeywordSearchRequest( - andKelimeler=andKelimeler, - orKelimeler=orKelimeler, - notAndKelimeler=notAndKelimeler, - notOrKelimeler=notOrKelimeler, - pageNumber=pageNumber, - pageSize=pageSize - ) - - logger.info(f"Tool 'search_danistay_by_keyword' called.") - try: - api_response = await danistay_client_instance.search_keyword_decisions(search_query) - if api_response.data: - return CompactDanistaySearchResult( - decisions=api_response.data.data, - total_records=api_response.data.recordsTotal, - requested_page=search_query.pageNumber, - page_size=search_query.pageSize) - 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) - except Exception as e: - logger.exception(f"Error in tool 'search_danistay_by_keyword'.") - raise - -@app.tool( - description="Search Danıştay decisions with detailed criteria (chamber selection, case numbers)", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -async def search_danistay_detailed( - daire: str = Field("", description="Chamber/Department name (e.g., '1. Daire')."), - esasYil: str = Field("", description="Case year for 'Esas No'."), - esasIlkSiraNo: str = Field("", description="Starting sequence for 'Esas No'."), - esasSonSiraNo: str = Field("", description="Ending sequence for 'Esas No'."), - kararYil: str = Field("", description="Decision year for 'Karar No'."), - kararIlkSiraNo: str = Field("", description="Starting sequence for 'Karar No'."), - kararSonSiraNo: str = Field("", description="Ending sequence for 'Karar No'."), - baslangicTarihi: str = Field("", description="Start date for decision (DD.MM.YYYY)."), - bitisTarihi: str = Field("", description="End date for decision (DD.MM.YYYY)."), - mevzuatNumarasi: str = Field("", description="Legislation number."), - mevzuatAdi: str = Field("", description="Legislation name."), - madde: str = Field("", description="Article number."), - pageNumber: int = Field(1, ge=1, description="Page number."), - # pageSize: int = Field(10, ge=1, le=10, description="Results per page.") -) -> CompactDanistaySearchResult: - # Search Danıştay decisions with detailed filtering. - - pageSize = 10 # Default value - - search_query = DanistayDetailedSearchRequest( - daire=daire, - esasYil=esasYil, - esasIlkSiraNo=esasIlkSiraNo, - esasSonSiraNo=esasSonSiraNo, - kararYil=kararYil, - kararIlkSiraNo=kararIlkSiraNo, - kararSonSiraNo=kararSonSiraNo, - baslangicTarihi=baslangicTarihi, - bitisTarihi=bitisTarihi, - mevzuatNumarasi=mevzuatNumarasi, - mevzuatAdi=mevzuatAdi, - madde=madde, - siralama="3", - siralamaDirection="desc", - pageNumber=pageNumber, - pageSize=pageSize - ) - - logger.info(f"Tool 'search_danistay_detailed' called.") - try: - api_response = await danistay_client_instance.search_detailed_decisions(search_query) - if api_response.data: - return CompactDanistaySearchResult( - decisions=api_response.data.data, - total_records=api_response.data.recordsTotal, - requested_page=search_query.pageNumber, - page_size=search_query.pageSize) - 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) - except Exception as e: - logger.exception(f"Error in tool 'search_danistay_detailed'.") - raise - -@app.tool( - description="Get Danıştay decision text in Markdown format", - annotations={ - "readOnlyHint": True, - "idempotentHint": True - } -) -async def get_danistay_document_markdown(id: str) -> DanistayDocumentMarkdown: - # Get Danıştay decision text as Markdown. Use ID from search results. - logger.info(f"Tool 'get_danistay_document_markdown' called for ID: {id}") - if not id or not id.strip(): raise ValueError("Document ID must be a non-empty string for Danıştay.") - try: - return await danistay_client_instance.get_decision_document_as_markdown(id) - except Exception as e: - logger.exception(f"Error in tool 'get_danistay_document_markdown'.") - raise -""" - -# --- MCP Tools for Emsal --- -@app.tool( - description="Search Emsal precedent decisions with detailed criteria", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -async def search_emsal_detailed_decisions( - keyword: str = Field("", description="Keyword to search."), - selected_bam_civil_court: str = Field("", description="Selected BAM Civil Court."), - selected_civil_court: str = Field("", description="Selected Civil Court."), - selected_regional_civil_chambers: List[str] = Field(default_factory=list, description="Selected Regional Civil Chambers."), - case_year_esas: str = Field("", description="Case year for 'Esas No'."), - case_start_seq_esas: str = Field("", description="Starting sequence for 'Esas No'."), - case_end_seq_esas: str = Field("", description="Ending sequence for 'Esas No'."), - decision_year_karar: str = Field("", description="Decision year for 'Karar No'."), - decision_start_seq_karar: str = Field("", description="Starting sequence for 'Karar No'."), - decision_end_seq_karar: str = Field("", description="Ending sequence for 'Karar No'."), - start_date: str = Field("", description="Start date for decision (DD.MM.YYYY)."), - end_date: str = Field("", description="End date for decision (DD.MM.YYYY)."), - sort_criteria: str = Field("1", description="Sorting criteria (e.g., 1: Esas No)."), - sort_direction: str = Field("desc", description="Sorting direction ('asc' or 'desc')."), - page_number: int = Field(1, ge=1, description="Page number (accepts int)."), - # page_size: int = Field(10, ge=1, le=10, description="Results per page.") -) -> CompactEmsalSearchResult: - """Search Emsal precedent decisions with detailed criteria.""" - - page_size = 10 # Default value - - search_query = EmsalSearchRequest( - keyword=keyword, - selected_bam_civil_court=selected_bam_civil_court, - selected_civil_court=selected_civil_court, - selected_regional_civil_chambers=selected_regional_civil_chambers, - case_year_esas=case_year_esas, - case_start_seq_esas=case_start_seq_esas, - case_end_seq_esas=case_end_seq_esas, - decision_year_karar=decision_year_karar, - decision_start_seq_karar=decision_start_seq_karar, - decision_end_seq_karar=decision_end_seq_karar, - start_date=start_date, - end_date=end_date, - sort_criteria=sort_criteria, - sort_direction=sort_direction, - page_number=page_number, - page_size=page_size - ) - - logger.info(f"Tool 'search_emsal_detailed_decisions' called.") - try: - api_response = await emsal_client_instance.search_detailed_decisions(search_query) - if api_response.data: - return CompactEmsalSearchResult( - decisions=api_response.data.data, - total_records=api_response.data.recordsTotal if api_response.data.recordsTotal is not None else 0, - requested_page=search_query.page_number, - page_size=search_query.page_size - ) - 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) - except Exception as e: - logger.exception(f"Error in tool 'search_emsal_detailed_decisions'.") - raise - -@app.tool( - description="Get Emsal precedent decision text in Markdown format", - annotations={ - "readOnlyHint": True, - "idempotentHint": True - } -) -async def get_emsal_document_markdown(id: str) -> EmsalDocumentMarkdown: - """Get document as Markdown.""" - logger.info(f"Tool 'get_emsal_document_markdown' called for ID: {id}") - if not id or not id.strip(): raise ValueError("Document ID required for Emsal.") - try: - return await emsal_client_instance.get_decision_document_as_markdown(id) - except Exception as e: - logger.exception(f"Error in tool 'get_emsal_document_markdown'.") - raise - -# --- MCP Tools for Uyusmazlik --- -@app.tool( - description="Search Uyuşmazlık Mahkemesi decisions for jurisdictional disputes", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -async def search_uyusmazlik_decisions( - icerik: str = Field("", description="Keyword or content for main text search."), - bolum: Literal["ALL", "Ceza Bölümü", "Genel Kurul Kararları", "Hukuk Bölümü"] = Field("ALL", description="Select the department (Bölüm). Use 'ALL' for all departments."), - uyusmazlik_turu: Literal["ALL", "Görev Uyuşmazlığı", "Hüküm Uyuşmazlığı"] = Field("ALL", description="Select the type of dispute. Use 'ALL' for all types."), - karar_sonuclari: List[Literal["Hüküm Uyuşmazlığı Olmadığına Dair", "Hüküm Uyuşmazlığı Olduğuna Dair"]] = Field(default_factory=list, description="List of desired 'Karar Sonucu' types."), - esas_yil: str = Field("", description="Case year ('Esas Yılı')."), - esas_sayisi: str = Field("", description="Case number ('Esas Sayısı')."), - karar_yil: str = Field("", description="Decision year ('Karar Yılı')."), - karar_sayisi: str = Field("", description="Decision number ('Karar Sayısı')."), - kanun_no: str = Field("", description="Relevant Law Number."), - karar_date_begin: str = Field("", description="Decision start date (DD.MM.YYYY)."), - karar_date_end: str = Field("", description="Decision end date (DD.MM.YYYY)."), - resmi_gazete_sayi: str = Field("", description="Official Gazette number."), - resmi_gazete_date: str = Field("", description="Official Gazette date (DD.MM.YYYY)."), - tumce: str = Field("", description="Exact phrase search."), - wild_card: str = Field("", description="Search for phrase and its inflections."), - hepsi: str = Field("", description="Search for texts containing all specified words."), - herhangi_birisi: str = Field("", description="Search for texts containing any of the specified words."), - not_hepsi: str = Field("", description="Exclude texts containing these specified words.") -) -> UyusmazlikSearchResponse: - """Search Court of Jurisdictional Disputes decisions.""" - - # Convert string literals to enums - # Map "ALL" to TUMU for backward compatibility - if bolum == "ALL": - bolum_enum = UyusmazlikBolumEnum.TUMU - else: - bolum_enum = UyusmazlikBolumEnum(bolum) if bolum else UyusmazlikBolumEnum.TUMU - - if uyusmazlik_turu == "ALL": - uyusmazlik_turu_enum = UyusmazlikTuruEnum.TUMU - else: - uyusmazlik_turu_enum = UyusmazlikTuruEnum(uyusmazlik_turu) if uyusmazlik_turu else UyusmazlikTuruEnum.TUMU - karar_sonuclari_enums = [UyusmazlikKararSonucuEnum(ks) for ks in karar_sonuclari] - - search_params = UyusmazlikSearchRequest( - icerik=icerik, - bolum=bolum_enum, - uyusmazlik_turu=uyusmazlik_turu_enum, - karar_sonuclari=karar_sonuclari_enums, - esas_yil=esas_yil, - esas_sayisi=esas_sayisi, - karar_yil=karar_yil, - karar_sayisi=karar_sayisi, - kanun_no=kanun_no, - karar_date_begin=karar_date_begin, - karar_date_end=karar_date_end, - resmi_gazete_sayi=resmi_gazete_sayi, - resmi_gazete_date=resmi_gazete_date, - tumce=tumce, - wild_card=wild_card, - hepsi=hepsi, - herhangi_birisi=herhangi_birisi, - not_hepsi=not_hepsi - ) - - logger.info(f"Tool 'search_uyusmazlik_decisions' called.") - try: - return await uyusmazlik_client_instance.search_decisions(search_params) - except Exception as e: - logger.exception(f"Error in tool 'search_uyusmazlik_decisions'.") - raise - -@app.tool( - description="Get Uyuşmazlık Mahkemesi decision text from URL in Markdown format", - annotations={ - "readOnlyHint": True, - "idempotentHint": True - } -) -async def get_uyusmazlik_document_markdown_from_url( - document_url: str = Field(..., description="Full URL to the Uyuşmazlık Mahkemesi decision document from search results") -) -> UyusmazlikDocumentMarkdown: - """Get Uyuşmazlık Mahkemesi decision as Markdown.""" - logger.info(f"Tool 'get_uyusmazlik_document_markdown_from_url' called for URL: {str(document_url)}") - if not document_url: - raise ValueError("Document URL (document_url) is required for Uyuşmazlık document retrieval.") - try: - return await uyusmazlik_client_instance.get_decision_document_as_markdown(str(document_url)) - except Exception as e: - logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url'.") - raise - -# --- DEACTIVATED: MCP Tools for Anayasa Mahkemesi (Individual Tools) --- -# Use search_anayasa_unified and get_anayasa_document_unified instead - -""" -@app.tool( - description="Search Constitutional Court norm control decisions with comprehensive filtering", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -# DEACTIVATED TOOL - Use search_anayasa_unified instead -# @app.tool( -# description="DEACTIVATED - Use search_anayasa_unified instead", -# annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True} -# ) -# async def search_anayasa_norm_denetimi_decisions(...) -> AnayasaSearchResult: -# raise ValueError("This tool is deactivated. Use search_anayasa_unified instead.") - -# DEACTIVATED TOOL - Use get_anayasa_document_unified instead -# @app.tool(...) -# async def get_anayasa_norm_denetimi_document_markdown(...) -> AnayasaDocumentMarkdown: -# raise ValueError("This tool is deactivated. Use get_anayasa_document_unified instead.") - -# DEACTIVATED TOOL - Use search_anayasa_unified instead -# @app.tool(...) -# async def search_anayasa_bireysel_basvuru_report(...) -> AnayasaBireyselReportSearchResult: -# raise ValueError("This tool is deactivated. Use search_anayasa_unified instead.") - -# DEACTIVATED TOOL - Use get_anayasa_document_unified instead -# @app.tool(...) -# async def get_anayasa_bireysel_basvuru_document_markdown(...) -> AnayasaBireyselBasvuruDocumentMarkdown: -# raise ValueError("This tool is deactivated. Use get_anayasa_document_unified instead.") -""" - -# --- Unified MCP Tools for Anayasa Mahkemesi --- -@app.tool( - description="Unified search for Constitutional Court decisions: both norm control (normkararlarbilgibankasi) and individual applications (kararlarbilgibankasi) in one tool", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -async def search_anayasa_unified( - decision_type: Literal["norm_denetimi", "bireysel_basvuru"] = Field(..., description="Decision type: norm_denetimi (norm control) or bireysel_basvuru (individual applications)"), - keywords: List[str] = Field(default_factory=list, description="Keywords to search for (common parameter)"), - page_to_fetch: int = Field(1, ge=1, le=100, description="Page number to fetch (1-100)"), - # results_per_page: int = Field(10, ge=1, le=100, description="Results per page (1-100)"), - - # Norm Denetimi specific parameters (ignored for bireysel_basvuru) - keywords_all: List[str] = Field(default_factory=list, description="All keywords must be present (norm_denetimi only)"), - keywords_any: List[str] = Field(default_factory=list, description="Any of these keywords (norm_denetimi only)"), - decision_type_norm: Literal["ALL", "1", "2", "3"] = Field("ALL", description="Decision type for norm denetimi"), - application_date_start: str = Field("", description="Application start date (norm_denetimi only)"), - application_date_end: str = Field("", description="Application end date (norm_denetimi only)"), - - # Bireysel Başvuru specific parameters (ignored for norm_denetimi) - decision_start_date: str = Field("", description="Decision start date (bireysel_basvuru only)"), - decision_end_date: str = Field("", description="Decision end date (bireysel_basvuru only)"), - norm_type: Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "0"] = Field("ALL", description="Norm type (bireysel_basvuru only)"), - subject_category: str = Field("", description="Subject category (bireysel_basvuru only)") -) -> str: - logger.info(f"Tool 'search_anayasa_unified' called for decision_type: {decision_type}") - - results_per_page = 10 # Default value - - try: - request = AnayasaUnifiedSearchRequest( - decision_type=decision_type, - keywords=keywords, - page_to_fetch=page_to_fetch, - results_per_page=results_per_page, - keywords_all=keywords_all, - keywords_any=keywords_any, - decision_type_norm=decision_type_norm, - application_date_start=application_date_start, - application_date_end=application_date_end, - decision_start_date=decision_start_date, - decision_end_date=decision_end_date, - norm_type=norm_type, - subject_category=subject_category - ) - - result = await anayasa_unified_client_instance.search_unified(request) - return json.dumps(result.model_dump(), ensure_ascii=False, indent=2) - - except Exception as e: - logger.exception(f"Error in tool 'search_anayasa_unified'.") - raise - -@app.tool( - description="Unified document retrieval for Constitutional Court decisions: auto-detects norm control vs individual applications based on URL", - annotations={ - "readOnlyHint": True, - "openWorldHint": False, - "idempotentHint": True - } -) -async def get_anayasa_document_unified( - document_url: str = Field(..., description="Document URL from search results"), - page_number: int = Field(1, ge=1, description="Page number for paginated content (1-indexed)") -) -> str: - logger.info(f"Tool 'get_anayasa_document_unified' called for URL: {document_url}, Page: {page_number}") - - try: - result = await anayasa_unified_client_instance.get_document_unified(document_url, page_number) - return json.dumps(result.model_dump(mode='json'), ensure_ascii=False, indent=2) - - except Exception as e: - logger.exception(f"Error in tool 'get_anayasa_document_unified'.") - raise - -# --- MCP Tools for KIK (Kamu İhale Kurulu) --- -@app.tool( - description="Search Public Procurement Authority (KİK) decisions for procurement law disputes", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -async def search_kik_decisions( - karar_tipi: Literal["rbUyusmazlik", "rbDuzenleyici", "rbMahkeme"] = Field("rbUyusmazlik", description="Type of KIK Decision."), - karar_no: str = Field("", description="Decision Number (e.g., '2024/UH.II-1766')."), - karar_tarihi_baslangic: str = Field("", description="Decision Date Start (DD.MM.YYYY)."), - karar_tarihi_bitis: str = Field("", description="Decision Date End (DD.MM.YYYY)."), - basvuru_sahibi: str = Field("", description="Applicant."), - ihaleyi_yapan_idare: str = Field("", description="Procuring Entity."), - basvuru_konusu_ihale: str = Field("", description="Tender subject of the application."), - karar_metni: str = Field("", description="Decision text search. Supports: +word, -word, \"exact phrase\", OR/AND"), - yil: str = Field("", description="Year of the decision."), - resmi_gazete_tarihi: str = Field("", description="Official Gazette Date (DD.MM.YYYY)."), - resmi_gazete_sayisi: str = Field("", description="Official Gazette Number."), - page: int = Field(1, ge=1, description="Results page number.") -) -> KikSearchResult: - """Search Public Procurement Authority (KIK) decisions.""" - - # Convert string literal to enum - karar_tipi_enum = KikKararTipi(karar_tipi) - - search_query = KikSearchRequest( - karar_tipi=karar_tipi_enum, - karar_no=karar_no, - karar_tarihi_baslangic=karar_tarihi_baslangic, - karar_tarihi_bitis=karar_tarihi_bitis, - basvuru_sahibi=basvuru_sahibi, - ihaleyi_yapan_idare=ihaleyi_yapan_idare, - basvuru_konusu_ihale=basvuru_konusu_ihale, - karar_metni=karar_metni, - yil=yil, - resmi_gazete_tarihi=resmi_gazete_tarihi, - resmi_gazete_sayisi=resmi_gazete_sayisi, - page=page - ) - - logger.info(f"Tool 'search_kik_decisions' called.") - 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.") - 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( - description="Get Public Procurement Authority (KİK) decision text in paginated Markdown format", - annotations={ - "readOnlyHint": True, - "idempotentHint": True - } -) -async def get_kik_document_markdown( - karar_id: str = Field(..., description="The Base64 encoded KIK decision identifier."), - page_number: int = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed). Default is 1.") -) -> KikDocumentMarkdown: - """Get KIK decision 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 - - try: - 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: - logger.exception(f"Error in KIK document retrieval tool 'get_kik_document_markdown' for karar_id: {karar_id}") - 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 - ) -@app.tool( - description="Search Competition Authority (Rekabet Kurumu) decisions for competition law and antitrust", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -async def search_rekabet_kurumu_decisions( - sayfaAdi: str = Field("", description="Search in decision title (Başlık)."), - YayinlanmaTarihi: str = Field("", description="Publication date (Yayım Tarihi), e.g., DD.MM.YYYY."), - PdfText: str = Field( - "", - description='Search in decision text. Use "\\"kesin cümle\\"" for precise matching.' - ), - KararTuru: Literal[ - "ALL", - "Birleşme ve Devralma", - "Diğer", - "Menfi Tespit ve Muafiyet", - "Özelleştirme", - "Rekabet İhlali" - ] = Field("ALL", description="Parameter description"), - KararSayisi: str = Field("", description="Decision number (Karar Sayısı)."), - KararTarihi: str = Field("", description="Decision date (Karar Tarihi), e.g., DD.MM.YYYY."), - page: int = Field(1, ge=1, description="Page number to fetch for the results list.") -) -> RekabetSearchResult: - """Search Competition Authority decisions.""" - - karar_turu_guid_enum = KARAR_TURU_ADI_TO_GUID_ENUM_MAP.get(KararTuru) - - try: - if karar_turu_guid_enum is None: - logger.warning(f"Invalid user-provided KararTuru: '{KararTuru}'. Defaulting to TUMU (all).") - karar_turu_guid_enum = RekabetKararTuruGuidEnum.TUMU - except Exception as e_map: - logger.error(f"Error mapping KararTuru '{KararTuru}': {e_map}. Defaulting to TUMU.") - karar_turu_guid_enum = RekabetKararTuruGuidEnum.TUMU - - search_query = RekabetKurumuSearchRequest( - sayfaAdi=sayfaAdi, - YayinlanmaTarihi=YayinlanmaTarihi, - PdfText=PdfText, - KararTuruID=karar_turu_guid_enum, - KararSayisi=KararSayisi, - KararTarihi=KararTarihi, - page=page - ) - logger.info(f"Tool 'search_rekabet_kurumu_decisions' called. Query: {search_query.model_dump_json(exclude_none=True, indent=2)}") - try: - - return await rekabet_client_instance.search_decisions(search_query) - except Exception as e: - logger.exception("Error in tool 'search_rekabet_kurumu_decisions'.") - return RekabetSearchResult(decisions=[], retrieved_page_number=page, total_records_found=0, total_pages=0) - -@app.tool( - description="Get Competition Authority decision text in paginated Markdown format", - annotations={ - "readOnlyHint": True, - "idempotentHint": True - } -) -async def get_rekabet_kurumu_document( - karar_id: str = Field(..., description="GUID (kararId) of the Rekabet Kurumu decision. This ID is obtained from search results."), - page_number: int = Field(1, ge=1, description="Requested page number for the Markdown content converted from PDF (1-indexed, accepts int). Default is 1.") -) -> RekabetDocument: - """Get Competition Authority decision as paginated Markdown.""" - logger.info(f"Tool 'get_rekabet_kurumu_document' called. Karar ID: {karar_id}, Markdown Page: {page_number}") - - current_page_to_fetch = page_number if page_number >= 1 else 1 - - try: - - return await rekabet_client_instance.get_decision_document(karar_id, page_number=current_page_to_fetch) - except Exception as e: - logger.exception(f"Error in tool 'get_rekabet_kurumu_document'. Karar ID: {karar_id}") - raise - -# --- MCP Tools for Bedesten (Unified Search Across All Courts) --- -@app.tool( - description="Search multiple Turkish courts (Yargıtay, Danıştay, Local Courts, Appeals Courts, KYB)", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -async def search_bedesten_unified( - ctx: Context, - phrase: str = Field(..., description="""Search query in Turkish. SUPPORTED OPERATORS: -• Simple: "mülkiyet hakkı" (finds both words) -• Exact phrase: "\"mülkiyet hakkı\"" (finds exact phrase) -• Required term: "+mülkiyet hakkı" (must contain mülkiyet) -• Exclude term: "mülkiyet -kira" (contains mülkiyet but not kira) -• Boolean AND: "mülkiyet AND hak" (both terms required) -• Boolean OR: "mülkiyet OR tapu" (either term acceptable) -• Boolean NOT: "mülkiyet NOT satış" (contains mülkiyet but not satış) -NOTE: Wildcards (*,?), regex patterns (/regex/), fuzzy search (~), and proximity search are NOT supported. -For best results, use exact phrases with quotes for legal terms."""), - court_types: List[BedestenCourtTypeEnum] = Field( - default=["YARGITAYKARARI", "DANISTAYKARAR"], - 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"), - 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) - • Danıştay: D1-D17 (1-17. Daire), DBGK (Büyük Gen.Kur.), IDDK (İdare Dava Daireleri Kurulu), VDDK (Vergi Dava Daireleri Kurulu), IBK (İçtihatları Birleştirme Kurulu), IIK (İdari İşler Kurulu), DBK (Başkanlar Kurulu), AYIM (Askeri Yüksek İdare Mahkemesi), AYIM1-3 (Askeri Yüksek İdare Mahkemesi 1-3. Daire) - """), - kararTarihiStart: str = Field("", description="Start date (ISO 8601 format)"), - kararTarihiEnd: str = Field("", description="End date (ISO 8601 format)") -) -> dict: - """Search Turkish legal databases via unified Bedesten API.""" - - # Get Bearer token information for access control and logging - try: - access_token: AccessToken = get_access_token() - user_id = access_token.client_id - user_scopes = access_token.scopes - - # Check for required scopes - if "yargi.read" not in user_scopes and "yargi.search" not in user_scopes: - raise ToolError(f"Insufficient permissions: 'yargi.read' or 'yargi.search' scope required. Current scopes: {user_scopes}") - - logger.info(f"Tool 'search_bedesten_unified' called by user '{user_id}' with scopes {user_scopes}") - - except Exception as e: - # Development mode fallback - allow access without strict token validation - logger.warning(f"Bearer token validation failed, using development mode: {str(e)}") - user_id = "dev-user" - user_scopes = ["yargi.read", "yargi.search"] - - pageSize = 10 # Default value - - search_data = BedestenSearchData( - pageSize=pageSize, - pageNumber=pageNumber, - itemTypeList=court_types, - phrase=phrase, - birimAdi=birimAdi, - kararTarihiStart=kararTarihiStart, - kararTarihiEnd=kararTarihiEnd - ) - - search_request = BedestenSearchRequest(data=search_data) - - logger.info(f"User '{user_id}' searching bedesten: phrase='{phrase}', court_types={court_types}, birimAdi='{birimAdi}', page={pageNumber}") - - try: - response = await bedesten_client_instance.search_documents(search_request) - - if response.data is None: - return { - "decisions": [], - "total_records": 0, - "requested_page": pageNumber, - "page_size": pageSize, - "searched_courts": court_types, - "error": "No data returned from Bedesten API" - } - - return { - "decisions": [d.model_dump() for d in response.data.emsalKararList], - "total_records": response.data.total, - "requested_page": pageNumber, - "page_size": pageSize, - "searched_courts": court_types - } - except Exception as e: - logger.exception("Error in tool 'search_bedesten_unified'") - raise - -@app.tool( - description="Get legal decision document from Bedesten API in Markdown format", - annotations={ - "readOnlyHint": True, - "idempotentHint": True - } -) -async def get_bedesten_document_markdown( - documentId: str = Field(..., description="Document ID from Bedesten search results") -) -> BedestenDocumentMarkdown: - """Get legal decision document as Markdown from Bedesten API.""" - logger.info(f"Tool 'get_bedesten_document_markdown' called for ID: {documentId}") - - if not documentId or not documentId.strip(): - raise ValueError("Document ID must be a non-empty string.") - - try: - return await bedesten_client_instance.get_document_as_markdown(documentId) - except Exception as e: - logger.exception("Error in tool 'get_kyb_bedesten_document_markdown'") - raise - -# --- MCP Tools for Sayıştay (Turkish Court of Accounts) --- - -# DEACTIVATED TOOL - Use search_sayistay_unified instead -# @app.tool( -# description="Search Sayıştay Genel Kurul decisions for audit and accountability regulations", -# annotations={ -# "readOnlyHint": True, -# "openWorldHint": True, -# "idempotentHint": True -# } -# ) -# async def search_sayistay_genel_kurul( -# karar_no: str = Field("", description="Decision number to search for (e.g., '5415')"), -# karar_ek: str = Field("", description="Decision appendix number (max 99, e.g., '1')"), -# karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY)"), -# karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY)"), -# karar_tamami: str = Field("", description="Full text search"), -# start: int = Field(0, description="Starting record for pagination (0-based)"), -# length: int = Field(10, description="Number of records per page (1-100)") -# ) -> GenelKurulSearchResponse: -# """Search Sayıştay General Assembly decisions.""" -# raise ValueError("This tool is deactivated. Use search_sayistay_unified instead.") - -# DEACTIVATED TOOL - Use search_sayistay_unified instead -# @app.tool( -# description="Search Sayıştay Temyiz Kurulu decisions with chamber filtering and comprehensive criteria", -# annotations={ -# "readOnlyHint": True, -# "openWorldHint": True, -# "idempotentHint": True -# } -# ) -# async def search_sayistay_temyiz_kurulu( -# ilam_dairesi: DaireEnum = Field("ALL", description="Audit chamber selection"), -# yili: str = Field("", description="Year (YYYY)"), -# karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY)"), -# karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY)"), -# kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Public admin type"), -# ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)"), -# dosya_no: str = Field("", description="File number for the case"), -# temyiz_tutanak_no: str = Field("", description="Appeals board meeting minutes number"), -# temyiz_karar: str = Field("", description="Appeals decision text"), -# web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Decision subject"), -# start: int = Field(0, description="Starting record for pagination (0-based)"), -# length: int = Field(10, description="Number of records per page (1-100)") -# ) -> TemyizKuruluSearchResponse: -# """Search Sayıştay Appeals Board decisions.""" -# raise ValueError("This tool is deactivated. Use search_sayistay_unified instead.") - -# DEACTIVATED TOOL - Use search_sayistay_unified instead -# @app.tool( -# description="Search Sayıştay Daire decisions with chamber filtering and subject categorization", -# annotations={ -# "readOnlyHint": True, -# "openWorldHint": True, -# "idempotentHint": True -# } -# ) -# async def search_sayistay_daire( -# yargilama_dairesi: DaireEnum = Field("ALL", description="Chamber selection"), -# karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY)"), -# karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY)"), -# ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)"), -# kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Public admin type"), -# hesap_yili: str = Field("", description="Fiscal year"), -# web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Decision subject"), -# web_karar_metni: str = Field("", description="Decision text search"), -# start: int = Field(0, description="Starting record for pagination (0-based)"), -# length: int = Field(10, description="Number of records per page (1-100)") -# ) -> DaireSearchResponse: -# """Search Sayıştay Chamber decisions.""" -# raise ValueError("This tool is deactivated. Use search_sayistay_unified instead.") - -# DEACTIVATED TOOL - Use get_sayistay_document_unified instead -# @app.tool( -# description="Get Sayıştay Genel Kurul decision document in Markdown format", -# annotations={ -# "readOnlyHint": True, -# "openWorldHint": False, -# "idempotentHint": True -# } -# ) -# async def get_sayistay_genel_kurul_document_markdown( -# decision_id: str = Field(..., description="Decision ID from search_sayistay_genel_kurul results") -# ) -> SayistayDocumentMarkdown: -# """Get Sayıştay General Assembly decision as Markdown.""" -# raise ValueError("This tool is deactivated. Use get_sayistay_document_unified instead.") - -# DEACTIVATED TOOL - Use get_sayistay_document_unified instead -# @app.tool( -# description="Get Sayıştay Temyiz Kurulu decision document in Markdown format", -# annotations={ -# "readOnlyHint": True, -# "openWorldHint": False, -# "idempotentHint": True -# } -# ) -# async def get_sayistay_temyiz_kurulu_document_markdown( -# decision_id: str = Field(..., description="Decision ID from search_sayistay_temyiz_kurulu results") -# ) -> SayistayDocumentMarkdown: -# """Get Sayıştay Appeals Board decision as Markdown.""" -# raise ValueError("This tool is deactivated. Use get_sayistay_document_unified instead.") - -# DEACTIVATED TOOL - Use get_sayistay_document_unified instead -# @app.tool( -# description="Get Sayıştay Daire decision document in Markdown format", -# annotations={ -# "readOnlyHint": True, -# "openWorldHint": False, -# "idempotentHint": True -# } -# ) -# async def get_sayistay_daire_document_markdown( -# decision_id: str = Field(..., description="Decision ID from search_sayistay_daire results") -# ) -> SayistayDocumentMarkdown: -# """Get Sayıştay Chamber decision as Markdown.""" -# raise ValueError("This tool is deactivated. Use get_sayistay_document_unified instead.") - -# --- UNIFIED MCP Tools for Sayıştay (Turkish Court of Accounts) --- - -@app.tool( - description="Search Sayıştay decisions unified across all three decision types (Genel Kurul, Temyiz Kurulu, Daire) with comprehensive filtering", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -async def search_sayistay_unified( - decision_type: Literal["genel_kurul", "temyiz_kurulu", "daire"] = Field(..., description="Decision type: genel_kurul, temyiz_kurulu, or daire"), - - # Common pagination parameters - start: int = Field(0, ge=0, description="Starting record for pagination (0-based)"), - length: int = Field(10, ge=1, le=100, description="Number of records per page (1-100)"), - - # Common search parameters - karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY format)"), - karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY format)"), - kamu_idaresi_turu: Literal["ALL", "Genel Bütçe Kapsamındaki İdareler", "Yüksek Öğretim Kurumları", "Diğer Özel Bütçeli İdareler", "Düzenleyici ve Denetleyici Kurumlar", "Sosyal Güvenlik Kurumları", "Özel İdareler", "Belediyeler ve Bağlı İdareler", "Diğer"] = Field("ALL", description="Public administration type filter"), - ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)"), - web_karar_konusu: Literal["ALL", "Harcırah Mevzuatı", "İhale Mevzuatı", "İş Mevzuatı", "Personel Mevzuatı", "Sorumluluk ve Yargılama Usulleri", "Vergi Resmi Harç ve Diğer Gelirler", "Çeşitli Konular"] = Field("ALL", description="Decision subject category filter"), - - # Genel Kurul specific parameters (ignored for other types) - karar_no: str = Field("", description="Decision number (genel_kurul only)"), - karar_ek: str = Field("", description="Decision appendix number (genel_kurul only)"), - karar_tamami: str = Field("", description="Full text search (genel_kurul only)"), - - # Temyiz Kurulu specific parameters (ignored for other types) - ilam_dairesi: Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8"] = Field("ALL", description="Audit chamber selection (temyiz_kurulu only)"), - yili: str = Field("", description="Year (YYYY format, temyiz_kurulu only)"), - dosya_no: str = Field("", description="File number (temyiz_kurulu only)"), - temyiz_tutanak_no: str = Field("", description="Appeals board meeting minutes number (temyiz_kurulu only)"), - temyiz_karar: str = Field("", description="Appeals decision text search (temyiz_kurulu only)"), - - # Daire specific parameters (ignored for other types) - yargilama_dairesi: Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8"] = Field("ALL", description="Chamber selection (daire only)"), - hesap_yili: str = Field("", description="Account year (daire only)"), - web_karar_metni: str = Field("", description="Decision text search (daire only)") -) -> SayistayUnifiedSearchResult: - """Search Sayıştay decisions across all three decision types with unified interface.""" - logger.info(f"Tool 'search_sayistay_unified' called with decision_type={decision_type}") - - try: - search_request = SayistayUnifiedSearchRequest( - decision_type=decision_type, - start=start, - length=length, - karar_tarih_baslangic=karar_tarih_baslangic, - karar_tarih_bitis=karar_tarih_bitis, - kamu_idaresi_turu=kamu_idaresi_turu, - ilam_no=ilam_no, - web_karar_konusu=web_karar_konusu, - karar_no=karar_no, - karar_ek=karar_ek, - karar_tamami=karar_tamami, - ilam_dairesi=ilam_dairesi, - yili=yili, - dosya_no=dosya_no, - temyiz_tutanak_no=temyiz_tutanak_no, - temyiz_karar=temyiz_karar, - yargilama_dairesi=yargilama_dairesi, - hesap_yili=hesap_yili, - web_karar_metni=web_karar_metni - ) - return await sayistay_unified_client_instance.search_unified(search_request) - except Exception as e: - logger.exception("Error in tool 'search_sayistay_unified'") - raise - -@app.tool( - description="Get Sayıştay decision document in Markdown format for any decision type", - annotations={ - "readOnlyHint": True, - "openWorldHint": False, - "idempotentHint": True - } -) -async def get_sayistay_document_unified( - decision_id: str = Field(..., description="Decision ID from search_sayistay_unified results"), - decision_type: Literal["genel_kurul", "temyiz_kurulu", "daire"] = Field(..., description="Decision type: genel_kurul, temyiz_kurulu, or daire") -) -> SayistayUnifiedDocumentMarkdown: - """Get Sayıştay decision document as Markdown for any decision type.""" - logger.info(f"Tool 'get_sayistay_document_unified' called for ID: {decision_id}, type: {decision_type}") - - if not decision_id or not decision_id.strip(): - raise ValueError("Decision ID must be a non-empty string.") - - try: - return await sayistay_unified_client_instance.get_document_unified(decision_id, decision_type) - except Exception as e: - logger.exception("Error in tool 'get_sayistay_document_unified'") - raise - -# --- Application Shutdown Handling --- -def perform_cleanup(): - logger.info("MCP Server performing cleanup...") - try: - loop = asyncio.get_event_loop_policy().get_event_loop() - if loop.is_closed(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - clients_to_close = [ - globals().get('yargitay_client_instance'), - globals().get('danistay_client_instance'), - globals().get('emsal_client_instance'), - globals().get('uyusmazlik_client_instance'), - globals().get('anayasa_norm_client_instance'), - globals().get('anayasa_bireysel_client_instance'), - globals().get('anayasa_unified_client_instance'), - globals().get('kik_client_instance'), - globals().get('rekabet_client_instance'), - globals().get('bedesten_client_instance'), - globals().get('sayistay_client_instance'), - globals().get('sayistay_unified_client_instance'), - globals().get('kvkk_client_instance'), - globals().get('bddk_client_instance') - ] - async def close_all_clients_async(): - tasks = [] - for client_instance in clients_to_close: - if client_instance and hasattr(client_instance, 'close_client_session') and callable(client_instance.close_client_session): - logger.info(f"Scheduling close for client session: {client_instance.__class__.__name__}") - 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: - logger.error(f"Error during atexit cleanup execution: {e}", exc_info=True) - logger.info("MCP Server atexit cleanup process finished.") - -atexit.register(perform_cleanup) - -# --- Health Check Tools --- -@app.tool( - description="Check if Turkish government legal database servers are operational", - annotations={ - "readOnlyHint": True, - "idempotentHint": True - } -) -async def check_government_servers_health() -> Dict[str, Any]: - """Check health status of Turkish government legal database servers.""" - logger.info("Health check tool called for government servers") - - health_results = {} - - # Check Yargıtay server - try: - yargitay_payload = { - "data": { - "aranan": "karar", - "arananKelime": "karar", - "pageSize": 10, - "pageNumber": 1 - } - } - - async with httpx.AsyncClient( - headers={ - "Accept": "*/*", - "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7", - "Connection": "keep-alive", - "Content-Type": "application/json; charset=UTF-8", - "Origin": "https://karararama.yargitay.gov.tr", - "Referer": "https://karararama.yargitay.gov.tr/", - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-origin", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", - "X-Requested-With": "XMLHttpRequest" - }, - timeout=30.0, - verify=False - ) as client: - response = await client.post( - "https://karararama.yargitay.gov.tr/aramalist", - json=yargitay_payload - ) - - if response.status_code == 200: - response_data = response.json() - records_total = response_data.get("data", {}).get("recordsTotal", 0) - - if records_total > 0: - health_results["yargitay"] = { - "status": "healthy", - "response_time_ms": response.elapsed.total_seconds() * 1000 - } - else: - health_results["yargitay"] = { - "status": "unhealthy", - "reason": "recordsTotal is 0 or missing", - "response_time_ms": response.elapsed.total_seconds() * 1000 - } - else: - health_results["yargitay"] = { - "status": "unhealthy", - "reason": f"HTTP {response.status_code}", - "response_time_ms": response.elapsed.total_seconds() * 1000 - } - - except Exception as e: - health_results["yargitay"] = { - "status": "unhealthy", - "reason": f"Connection error: {str(e)}" - } - - # Check Bedesten API server - try: - bedesten_payload = { - "data": { - "pageSize": 5, - "pageNumber": 1, - "itemTypeList": ["YARGITAYKARARI"], - "phrase": "karar", - "sortFields": ["KARAR_TARIHI"], - "sortDirection": "desc" - }, - "applicationName": "UyapMevzuat", - "paging": True - } - - async with httpx.AsyncClient( - headers={ - "Content-Type": "application/json", - "Accept": "application/json", - "User-Agent": "Mozilla/5.0 Health Check" - }, - timeout=30.0, - verify=False - ) as client: - response = await client.post( - "https://bedesten.adalet.gov.tr/emsal-karar/searchDocuments", - json=bedesten_payload - ) - - if response.status_code == 200: - response_data = response.json() - logger.debug(f"Bedesten API response: {response_data}") - if response_data and isinstance(response_data, dict): - data_section = response_data.get("data") - if data_section and isinstance(data_section, dict): - total_found = data_section.get("total", 0) - else: - total_found = 0 - else: - total_found = 0 - - if total_found > 0: - health_results["bedesten"] = { - "status": "healthy", - "response_time_ms": response.elapsed.total_seconds() * 1000 - } - else: - health_results["bedesten"] = { - "status": "unhealthy", - "reason": "total is 0 or missing in data field", - "response_time_ms": response.elapsed.total_seconds() * 1000 - } - else: - health_results["bedesten"] = { - "status": "unhealthy", - "reason": f"HTTP {response.status_code}", - "response_time_ms": response.elapsed.total_seconds() * 1000 - } - - except Exception as e: - health_results["bedesten"] = { - "status": "unhealthy", - "reason": f"Connection error: {str(e)}" - } - - # Overall health assessment - healthy_servers = sum(1 for server in health_results.values() if server["status"] == "healthy") - total_servers = len(health_results) - - overall_status = "healthy" if healthy_servers == total_servers else "degraded" if healthy_servers > 0 else "unhealthy" - - return { - "overall_status": overall_status, - "healthy_servers": healthy_servers, - "total_servers": total_servers, - "servers": health_results, - "check_timestamp": f"{__import__('datetime').datetime.now().isoformat()}" - } - -# --- MCP Tools for KVKK --- -@app.tool( - description="Search KVKK data protection authority decisions", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -async def search_kvkk_decisions( - keywords: str = Field(..., description="Turkish keywords. Supports +required -excluded \"exact phrase\" operators"), - page: int = Field(1, ge=1, le=50, description="Page number for results (1-50)."), - # pageSize: int = Field(10, ge=1, le=20, description="Number of results per page (1-20).") -) -> KvkkSearchResult: - """Search function for legal decisions.""" - logger.info(f"KVKK search tool called with keywords: {keywords}") - - pageSize = 10 # Default value - - search_request = KvkkSearchRequest( - keywords=keywords, - page=page, - pageSize=pageSize - ) - - try: - result = await kvkk_client_instance.search_decisions(search_request) - logger.info(f"KVKK search completed. Found {len(result.decisions)} decisions on page {page}") - return result - except Exception as e: - logger.exception(f"Error in KVKK search: {e}") - # Return empty result on error - return KvkkSearchResult( - decisions=[], - total_results=0, - page=page, - pageSize=pageSize, - query=keywords - ) - -@app.tool( - description="Get KVKK decision document in Markdown format with metadata extraction", - annotations={ - "readOnlyHint": True, - "openWorldHint": False, - "idempotentHint": True - } -) -async def get_kvkk_document_markdown( - decision_url: str = Field(..., description="KVKK decision URL from search results"), - page_number: int = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed, accepts int). Default is 1 (first 5,000 characters).") -) -> KvkkDocumentMarkdown: - """Get KVKK decision as paginated Markdown.""" - logger.info(f"KVKK document retrieval tool called for URL: {decision_url}") - - if not decision_url or not decision_url.strip(): - return KvkkDocumentMarkdown( - source_url=HttpUrl("https://www.kvkk.gov.tr"), - title=None, - decision_date=None, - decision_number=None, - subject_summary=None, - markdown_chunk=None, - current_page=page_number or 1, - total_pages=0, - is_paginated=False, - error_message="Decision URL is required and cannot be empty." - ) - - try: - # Validate URL format - if not decision_url.startswith("https://www.kvkk.gov.tr/"): - return KvkkDocumentMarkdown( - source_url=HttpUrl(decision_url), - title=None, - decision_date=None, - decision_number=None, - subject_summary=None, - markdown_chunk=None, - current_page=page_number or 1, - total_pages=0, - is_paginated=False, - error_message="Invalid KVKK decision URL format. URL must start with https://www.kvkk.gov.tr/" - ) - - result = await kvkk_client_instance.get_decision_document(decision_url, page_number or 1) - logger.info(f"KVKK document retrieved successfully. Page {result.current_page}/{result.total_pages}, Content length: {len(result.markdown_chunk) if result.markdown_chunk else 0}") - return result - - except Exception as e: - logger.exception(f"Error retrieving KVKK document: {e}") - return KvkkDocumentMarkdown( - source_url=HttpUrl(decision_url), - title=None, - decision_date=None, - decision_number=None, - subject_summary=None, - markdown_chunk=None, - current_page=page_number or 1, - total_pages=0, - is_paginated=False, - error_message=f"Error retrieving KVKK document: {str(e)}" - ) - -# --- MCP Tools for BDDK (Banking Regulation Authority) --- -@app.tool( - description="Search BDDK banking regulation decisions", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -async def search_bddk_decisions( - keywords: str = Field(..., description="Search keywords in Turkish"), - page: int = Field(1, ge=1, description="Page number") - # pageSize: int = Field(10, ge=1, le=50, description="Results per page") -) -> dict: - """Search BDDK banking regulation and supervision decisions.""" - logger.info(f"BDDK search tool called with keywords: {keywords}, page: {page}") - - pageSize = 10 # Default value - - try: - search_request = BddkSearchRequest( - keywords=keywords, - page=page, - pageSize=pageSize - ) - - result = await bddk_client_instance.search_decisions(search_request) - logger.info(f"BDDK search completed. Found {len(result.decisions)} decisions on page {page}") - - return { - "decisions": [ - { - "title": dec.title, - "document_id": dec.document_id, - "content": dec.content - } - for dec in result.decisions - ], - "total_results": result.total_results, - "page": result.page, - "pageSize": result.pageSize - } - - except Exception as e: - logger.exception(f"Error searching BDDK decisions: {e}") - return { - "decisions": [], - "total_results": 0, - "page": page, - "pageSize": pageSize, - "error": str(e) - } - -@app.tool( - description="Get BDDK decision document as Markdown", - annotations={ - "readOnlyHint": True, - "openWorldHint": False, - "idempotentHint": True - } -) -async def get_bddk_document_markdown( - document_id: str = Field(..., description="BDDK document ID (e.g., '310')"), - page_number: int = Field(1, ge=1, description="Page number") -) -> dict: - """Retrieve BDDK decision document in Markdown format.""" - logger.info(f"BDDK document retrieval tool called for ID: {document_id}, page: {page_number}") - - if not document_id or not document_id.strip(): - return { - "document_id": document_id, - "markdown_content": "", - "page_number": page_number, - "total_pages": 0, - "error": "Document ID is required" - } - - try: - result = await bddk_client_instance.get_document_markdown(document_id, page_number) - logger.info(f"BDDK document retrieved successfully. Page {result.page_number}/{result.total_pages}") - - return { - "document_id": result.document_id, - "markdown_content": result.markdown_content, - "page_number": result.page_number, - "total_pages": result.total_pages - } - - except Exception as e: - logger.exception(f"Error retrieving BDDK document: {e}") - return { - "document_id": document_id, - "markdown_content": "", - "page_number": page_number, - "total_pages": 0, - "error": str(e) - } - -# --- 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() - -@app.tool( - description="DO NOT USE unless you are ChatGPT Deep Research. Search Turkish courts (Turkish keywords only). Supports: +term (must have), -term (exclude), \"exact phrase\", term1 OR term2", - annotations={ - "readOnlyHint": True, - "openWorldHint": True, - "idempotentHint": True - } -) -async def search( - query: str = Field(..., description="Turkish search query") -) -> Dict[str, List[Dict[str, str]]]: - """ - Bedesten API search tool for ChatGPT Deep Research compatibility. - - This tool searches Turkish legal databases via the unified Bedesten API. - It supports advanced search operators and covers all major court types. - - USAGE RESTRICTION: Only for ChatGPT Deep Research workflows. - For regular legal research, use search_bedesten_unified with specific court types. - - Returns: - Object with "results" field containing a list of documents with id, title, text preview, and url - as required by ChatGPT Deep Research specification. - """ - logger.info(f"ChatGPT Deep Research search tool called with query: {query}") - - results = [] - - 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") - ] - - for item_type, court_name, id_prefix 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, - pageNumber=1 - ) - ) - ) - - # Handle potential None data - if search_results.data is None: - 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) - 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}" - }) - - if search_results.data: - logger.info(f"Found {len(search_results.data.emsalKararList)} results from {court_name}") - else: - logger.info(f"Found 0 results from {court_name} (no data returned)") - - except Exception as e: - logger.warning(f"Bedesten API search error for {court_name}: {e}") - - # Comment out other API implementations for ChatGPT Deep Research - """ - # Other API implementations disabled for ChatGPT Deep Research - # These are available through specific court tools: - - # Yargıtay Official API - use search_yargitay_detailed instead - # 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 - # Court of Accounts - use search_sayistay_* tools instead - # UYAP Emsal - use search_emsal_detailed_decisions instead - # Jurisdictional Disputes Court - use search_uyusmazlik_decisions instead - """ - - logger.info(f"ChatGPT Deep Research search completed. Found {len(results)} results via Bedesten API.") - return {"results": results} - - except Exception as e: - logger.exception("Error in ChatGPT Deep Research search tool") - # Return partial results if any were found - if results: - return {"results": results} - raise - -@app.tool( - description="DO NOT USE unless you are ChatGPT Deep Research. Fetch document by ID. See docs for details", - annotations={ - "readOnlyHint": True, - "openWorldHint": False, # Retrieves specific documents, not exploring - "idempotentHint": True - } -) -async def fetch( - id: str = Field(..., description="Document identifier from search results (numeric only)") -) -> Dict[str, Any]: - """ - Bedesten API fetch tool for ChatGPT Deep Research compatibility. - - Retrieves the full text content of Turkish legal documents via unified Bedesten API. - Converts documents from HTML/PDF to clean Markdown format. - - USAGE RESTRICTION: Only for ChatGPT Deep Research workflows. - For regular legal research, use specific court document tools. - - Input Format: - - id: Numeric document identifier from search results (e.g., "730113500", "71370900") - - Returns: - Single object with numeric id, title, text (full Markdown content), mevzuat.adalet.gov.tr url, and metadata fields - as required by ChatGPT Deep Research specification. - """ - logger.info(f"ChatGPT Deep Research fetch tool called for document ID: {id}") - - if not id or not id.strip(): - raise ValueError("Document ID must be a non-empty string") - - try: - # 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 - 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}") - - return { - "id": id, - "title": title, - "text": doc.markdown_content, - "url": f"https://mevzuat.adalet.gov.tr/ictihat/{id}", - "metadata": { - "database": "Turkish Legal Database via Bedesten API", - "document_id": id, - "source_url": doc.source_url, - "mime_type": doc.mime_type, - "api_source": "Bedesten Unified API", - "chatgpt_deep_research": True - } - } - - # Comment out other API implementations for ChatGPT Deep Research - """ - # Other API implementations disabled for ChatGPT Deep Research - # These are available through specific court document tools: - - elif id.startswith("yargitay_"): - # Yargıtay Official API - use get_yargitay_document_markdown instead - doc_id = id.replace("yargitay_", "") - doc = await yargitay_client_instance.get_decision_document_as_markdown(doc_id) - - elif id.startswith("danistay_"): - # Danıştay Official API - use get_danistay_document_markdown instead - doc_id = id.replace("danistay_", "") - doc = await danistay_client_instance.get_decision_document_as_markdown(doc_id) - - elif id.startswith("anayasa_"): - # Constitutional Court - use get_anayasa_norm_denetimi_document_markdown instead - doc_id = id.replace("anayasa_", "") - doc = await anayasa_norm_client_instance.get_decision_document_as_markdown(...) - - elif id.startswith("rekabet_"): - # Competition Authority - use get_rekabet_kurumu_document instead - doc_id = id.replace("rekabet_", "") - doc = await rekabet_client_instance.get_decision_document(...) - - elif id.startswith("kik_"): - # Public Procurement Authority - use get_kik_decision_document_as_markdown instead - doc_id = id.replace("kik_", "") - doc = await kik_client_instance.get_decision_document_as_markdown(doc_id) - - elif id.startswith("local_"): - # This was already using Bedesten API, but deprecated for ChatGPT Deep Research - doc_id = id.replace("local_", "") - doc = await bedesten_client_instance.get_document_as_markdown(doc_id) - """ - - except Exception as e: - logger.exception(f"Error fetching ChatGPT Deep Research document {id}") - raise - -# --- Token Metrics Tool Removed for Optimization --- - -def ensure_playwright_browsers(): - """Ensure Playwright browsers are installed for KIK tool functionality.""" - try: - import subprocess - import os - - # Check if chromium is already installed - chromium_path = os.path.expanduser("~/Library/Caches/ms-playwright/chromium-1179") - if os.path.exists(chromium_path): - logger.info("Playwright Chromium browser already installed.") - return - - logger.info("Installing Playwright Chromium browser for KIK tool...") - result = subprocess.run( - ["python", "-m", "playwright", "install", "chromium"], - capture_output=True, - text=True, - timeout=300 # 5 minutes timeout - ) - - if result.returncode == 0: - logger.info("Playwright Chromium browser installed successfully.") - else: - logger.warning(f"Failed to install Playwright browser: {result.stderr}") - logger.warning("KIK tool may not work properly without Playwright browsers.") - - except Exception as e: - logger.warning(f"Could not auto-install Playwright browsers: {e}") - logger.warning("KIK tool may not work properly. Manual installation: 'playwright install chromium'") - -def main(): - logger.info(f"Starting {app.name} server via main() function...") - logger.info(f"Logs will be written to: {LOG_FILE_PATH}") - - # Ensure Playwright browsers are installed - ensure_playwright_browsers() - - try: - app.run() - except KeyboardInterrupt: - logger.info("Server shut down by user (KeyboardInterrupt).") - except Exception as e: - logger.exception("Server failed to start or crashed.") - finally: - logger.info(f"{app.name} server has shut down.") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/nginx.conf b/saidsurucu-yargi-mcp-f5fa007/nginx.conf deleted file mode 100644 index 5cb39b2..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/nginx.conf +++ /dev/null @@ -1,94 +0,0 @@ -events { - worker_connections 1024; -} - -http { - upstream yargi_mcp { - server yargi-mcp:8000; - } - - # Rate limiting - limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; - limit_req_zone $binary_remote_addr zone=mcp_limit:10m rate=100r/s; - - server { - listen 80; - server_name localhost; - - # Redirect HTTP to HTTPS in production - # return 301 https://$server_name$request_uri; - - # Security headers - add_header X-Content-Type-Options nosniff; - add_header X-Frame-Options DENY; - add_header X-XSS-Protection "1; mode=block"; - add_header Referrer-Policy "strict-origin-when-cross-origin"; - - # API endpoints - location /api/ { - limit_req zone=api_limit burst=20 nodelay; - - proxy_pass http://yargi_mcp; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # Timeouts - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - } - - # MCP endpoint (higher rate limit) - location /mcp-server/mcp/ { - limit_req zone=mcp_limit burst=50 nodelay; - - proxy_pass http://yargi_mcp; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # WebSocket support - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - # Longer timeouts for MCP operations - proxy_connect_timeout 300s; - proxy_send_timeout 300s; - proxy_read_timeout 300s; - } - - # Health check (no rate limit) - location /health { - proxy_pass http://yargi_mcp; - proxy_set_header Host $host; - } - - # Root and other paths - location / { - limit_req zone=api_limit burst=10 nodelay; - - proxy_pass http://yargi_mcp; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - } - - # SSL configuration (uncomment for production) - # server { - # listen 443 ssl http2; - # server_name your-domain.com; - # - # ssl_certificate /etc/nginx/ssl/cert.pem; - # ssl_certificate_key /etc/nginx/ssl/key.pem; - # ssl_protocols TLSv1.2 TLSv1.3; - # ssl_ciphers HIGH:!aNULL:!MD5; - # - # # Include all location blocks from above - # } -} \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/ornek.png b/saidsurucu-yargi-mcp-f5fa007/ornek.png deleted file mode 100644 index 012d2ef..0000000 Binary files a/saidsurucu-yargi-mcp-f5fa007/ornek.png and /dev/null differ diff --git a/saidsurucu-yargi-mcp-f5fa007/pyproject.toml b/saidsurucu-yargi-mcp-f5fa007/pyproject.toml deleted file mode 100644 index e90bea6..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/pyproject.toml +++ /dev/null @@ -1,66 +0,0 @@ -[project] -name = "yargi-mcp" -version = "0.1.6" -description = "MCP Server For Turkish Legal Databases" -readme = "README.md" -requires-python = ">=3.11" -license = {text = "MIT"} -authors = [{name = "Said Surucu", email = "saidsrc@gmail.com"}] -keywords = ["mcp", "turkish-law", "legal", "yargitay", "danistay", "bddk", "kvkk", "turkish", "law", "court", "decisions"] -classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Legal Industry", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Text Processing :: Markup :: Markdown", - "Operating System :: OS Independent", -] -urls = {Homepage = "https://github.com/saidsurucu/yargi-mcp", Issues = "https://github.com/saidsurucu/yargi-mcp/issues"} -dependencies = [ - "beautifulsoup4>=4.13.4", - "httpx>=0.28.1", - "markitdown[pdf]>=0.1.1", - "pydantic>=2.11.4", - "aiohttp>=3.11.18", - "playwright>=1.52.0", - "fastmcp>=2.10.5", - "pypdf>=5.5.0", - "fastapi>=0.115.14", - "PyJWT>=2.8.0", - "tiktoken>=0.5.0", -] - -[project.optional-dependencies] -asgi = [ - "uvicorn[standard]>=0.30.0", - "starlette>=0.37.0", -] -api = [ - "fastapi>=0.115.0", - "uvicorn[standard]>=0.30.0", -] -production = [ - "gunicorn>=22.0.0", - "uvicorn[standard]>=0.30.0", -] -saas = [ - "clerk-backend-api>=3.0.0", - "stripe>=9.1.0", - "upstash-redis>=1.1.0", -] - -[project.scripts] -yargi-mcp = "mcp_server_main:main" - -[tool.setuptools] -py-modules = ["mcp_server_main", "mcp_auth_factory", "mcp_auth_http_adapter", "asgi_app", "fastapi_app", "starlette_app", "run_asgi", "stripe_webhook"] - -[tool.setuptools.packages.find] -include = ["*_mcp_module", "mcp_auth"] - -[build-system] -requires = ["setuptools>=65.0", "wheel"] -build-backend = "setuptools.build_meta" diff --git a/saidsurucu-yargi-mcp-f5fa007/railway.json b/saidsurucu-yargi-mcp-f5fa007/railway.json deleted file mode 100644 index 0c54927..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/railway.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://railway.app/railway.schema.json", - "build": { - "builder": "NIXPACKS", - "buildCommand": "pip install -e .[asgi]" - }, - "deploy": { - "startCommand": "uvicorn asgi_app:app --host 0.0.0.0 --port $PORT", - "healthcheckPath": "/health", - "healthcheckTimeout": 30, - "restartPolicyType": "ON_FAILURE", - "restartPolicyMaxRetries": 3 - }, - "variables": { - "ALLOWED_ORIGINS": "*", - "LOG_LEVEL": "info" - } -} \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/redis_session_store.py b/saidsurucu-yargi-mcp-f5fa007/redis_session_store.py deleted file mode 100644 index 0eb1142..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/redis_session_store.py +++ /dev/null @@ -1,464 +0,0 @@ -""" -Redis Session Store for OAuth Authorization Codes and User Sessions - -This module provides Redis-based storage for OAuth authorization codes and user sessions, -enabling multi-machine deployment support by replacing in-memory storage. - -Uses Upstash Redis via REST API for serverless-friendly operation. -""" - -import os -import json -import time -import logging -from typing import Optional, Dict, Any, Union -from datetime import datetime, timedelta - -logger = logging.getLogger(__name__) - -try: - from upstash_redis import Redis - UPSTASH_AVAILABLE = True -except ImportError: - UPSTASH_AVAILABLE = False - Redis = None - -# Use standard Python exceptions for Redis connection errors -import socket -from requests.exceptions import ConnectionError as RequestsConnectionError, Timeout as RequestsTimeout - -class RedisSessionStore: - """ - Redis-based session store for OAuth flows and user sessions. - - Uses Upstash Redis REST API for connection-free operation suitable for - multi-instance deployments on platforms like Fly.io. - """ - - def __init__(self): - """Initialize Redis connection using environment variables.""" - if not UPSTASH_AVAILABLE: - raise ImportError("upstash-redis package is required. Install with: pip install upstash-redis") - - # Initialize Upstash Redis client from environment with optimized connection settings - try: - # Get Upstash Redis configuration - redis_url = os.getenv("UPSTASH_REDIS_REST_URL") - redis_token = os.getenv("UPSTASH_REDIS_REST_TOKEN") - - if not redis_url or not redis_token: - raise ValueError("UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN must be set") - - logger.info(f"Connecting to Upstash Redis at {redis_url[:30]}...") - - # Initialize with explicit configuration for better SSL handling - self.redis = Redis( - url=redis_url, - token=redis_token - ) - - logger.info("Upstash Redis client created") - - # Skip connection test during initialization to prevent server hang - # Connection will be tested during first actual operation - logger.info("Redis client initialized - connection will be tested on first use") - - except Exception as e: - logger.error(f"Failed to initialize Upstash Redis: {e}") - raise - - # TTL values (in seconds) - self.oauth_code_ttl = int(os.getenv("OAUTH_CODE_TTL", "600")) # 10 minutes - self.session_ttl = int(os.getenv("SESSION_TTL", "3600")) # 1 hour - - def _serialize_data(self, data: Dict[str, Any]) -> Dict[str, str]: - """Convert data to Redis-compatible string format.""" - serialized = {} - for key, value in data.items(): - if isinstance(value, (dict, list)): - serialized[key] = json.dumps(value) - elif isinstance(value, (int, float)): - serialized[key] = str(value) - elif isinstance(value, bool): - serialized[key] = "true" if value else "false" - else: - serialized[key] = str(value) - return serialized - - def _deserialize_data(self, data: Dict[str, str]) -> Dict[str, Any]: - """Convert Redis string data back to original types.""" - if not data: - return {} - - deserialized = {} - for key, value in data.items(): - if not isinstance(value, str): - deserialized[key] = value - continue - - # Try to deserialize JSON - if value.startswith(('[', '{')): - try: - deserialized[key] = json.loads(value) - continue - except json.JSONDecodeError: - pass - - # Try to convert numbers - if value.isdigit(): - deserialized[key] = int(value) - continue - - if value.replace('.', '').isdigit(): - try: - deserialized[key] = float(value) - continue - except ValueError: - pass - - # Handle booleans - if value in ("true", "false"): - deserialized[key] = value == "true" - continue - - # Keep as string - deserialized[key] = value - - return deserialized - - # OAuth Authorization Code Methods - - def set_oauth_code(self, code: str, data: Dict[str, Any]) -> bool: - """ - Store OAuth authorization code with automatic expiration. - - Args: - code: Authorization code string - data: Code data including user_id, client_id, etc. - - Returns: - True if stored successfully, False otherwise - """ - try: - key = f"oauth:code:{code}" - - # Add timestamp for debugging - data_with_timestamp = data.copy() - data_with_timestamp.update({ - "created_at": time.time(), - "expires_at": time.time() + self.oauth_code_ttl - }) - - # Serialize and store - Upstash Redis doesn't support mapping parameter - serialized_data = self._serialize_data(data_with_timestamp) - - # Use individual hset calls for each field with retry logic - max_retries = 3 - for attempt in range(max_retries): - try: - # Clear any existing data first - self.redis.delete(key) - - # Set all fields in a pipeline-like manner - for field, value in serialized_data.items(): - self.redis.hset(key, field, value) - - # Set expiration - self.redis.expire(key, self.oauth_code_ttl) - - logger.info(f"Stored OAuth code {code[:10]}... with TTL {self.oauth_code_ttl}s (attempt {attempt + 1})") - return True - - except (RequestsConnectionError, RequestsTimeout, OSError, socket.error) as e: - logger.warning(f"Redis connection error on attempt {attempt + 1}: {e}") - if attempt == max_retries - 1: - raise # Re-raise on final attempt - time.sleep(0.5 * (attempt + 1)) # Exponential backoff - - except Exception as e: - logger.error(f"Failed to store OAuth code {code[:10]}... after {max_retries} attempts: {e}") - return False - - def get_oauth_code(self, code: str, delete_after_use: bool = True) -> Optional[Dict[str, Any]]: - """ - Retrieve OAuth authorization code data. - - Args: - code: Authorization code string - delete_after_use: If True, delete the code after retrieval (one-time use) - - Returns: - Code data dictionary or None if not found/expired - """ - max_retries = 3 - for attempt in range(max_retries): - try: - key = f"oauth:code:{code}" - - # Get all hash fields with retry - data = self.redis.hgetall(key) - - if not data: - logger.warning(f"OAuth code {code[:10]}... not found or expired (attempt {attempt + 1})") - return None - - # Deserialize data - deserialized_data = self._deserialize_data(data) - - # Check manual expiration (in case Redis TTL failed) - expires_at = deserialized_data.get("expires_at", 0) - if expires_at and time.time() > expires_at: - logger.warning(f"OAuth code {code[:10]}... manually expired") - try: - self.redis.delete(key) - except Exception as del_error: - logger.warning(f"Failed to delete expired code: {del_error}") - return None - - # Delete after use for security (one-time use) - if delete_after_use: - try: - self.redis.delete(key) - logger.info(f"Retrieved and deleted OAuth code {code[:10]}... (attempt {attempt + 1})") - except Exception as del_error: - logger.warning(f"Failed to delete code after use: {del_error}") - # Continue anyway since we got the data - else: - logger.info(f"Retrieved OAuth code {code[:10]}... (not deleted, attempt {attempt + 1})") - - return deserialized_data - - except (RequestsConnectionError, RequestsTimeout, OSError, socket.error) as e: - logger.warning(f"Redis connection error on retrieval attempt {attempt + 1}: {e}") - if attempt == max_retries - 1: - logger.error(f"Failed to retrieve OAuth code {code[:10]}... after {max_retries} attempts: {e}") - return None - time.sleep(0.5 * (attempt + 1)) # Exponential backoff - - except Exception as e: - logger.error(f"Failed to retrieve OAuth code {code[:10]}... on attempt {attempt + 1}: {e}") - if attempt == max_retries - 1: - return None - time.sleep(0.5 * (attempt + 1)) - - return None - - # User Session Methods - - def set_session(self, session_id: str, user_data: Dict[str, Any]) -> bool: - """ - Store user session data with sliding expiration. - - Args: - session_id: Unique session identifier - user_data: User session data (user_id, email, scopes, etc.) - - Returns: - True if stored successfully, False otherwise - """ - try: - key = f"session:{session_id}" - - # Add session metadata - session_data = user_data.copy() - session_data.update({ - "session_id": session_id, - "created_at": time.time(), - "last_accessed": time.time() - }) - - # Serialize and store - Upstash Redis doesn't support mapping parameter - serialized_data = self._serialize_data(session_data) - - # Use individual hset calls for each field (Upstash compatibility) - for field, value in serialized_data.items(): - self.redis.hset(key, field, value) - self.redis.expire(key, self.session_ttl) - - logger.info(f"Stored session {session_id[:10]}... with TTL {self.session_ttl}s") - return True - - except Exception as e: - logger.error(f"Failed to store session {session_id[:10]}...: {e}") - return False - - def get_session(self, session_id: str, refresh_ttl: bool = True) -> Optional[Dict[str, Any]]: - """ - Retrieve user session data. - - Args: - session_id: Session identifier - refresh_ttl: If True, extend session TTL on access - - Returns: - Session data dictionary or None if not found/expired - """ - try: - key = f"session:{session_id}" - - # Get session data - data = self.redis.hgetall(key) - - if not data: - logger.warning(f"Session {session_id[:10]}... not found or expired") - return None - - # Deserialize data - session_data = self._deserialize_data(data) - - # Update last accessed time and refresh TTL - if refresh_ttl: - session_data["last_accessed"] = time.time() - self.redis.hset(key, "last_accessed", str(time.time())) - self.redis.expire(key, self.session_ttl) - logger.debug(f"Refreshed session {session_id[:10]}... TTL") - - return session_data - - except Exception as e: - logger.error(f"Failed to retrieve session {session_id[:10]}...: {e}") - return None - - def delete_session(self, session_id: str) -> bool: - """ - Delete user session (logout). - - Args: - session_id: Session identifier - - Returns: - True if deleted successfully, False otherwise - """ - try: - key = f"session:{session_id}" - result = self.redis.delete(key) - - if result: - logger.info(f"Deleted session {session_id[:10]}...") - return True - else: - logger.warning(f"Session {session_id[:10]}... not found for deletion") - return False - - except Exception as e: - logger.error(f"Failed to delete session {session_id[:10]}...: {e}") - return False - - # Health Check Methods - - def health_check(self) -> Dict[str, Any]: - """ - Perform Redis health check. - - Returns: - Health status dictionary - """ - try: - # Test basic operations - test_key = f"health:check:{int(time.time())}" - test_value = {"timestamp": time.time(), "test": True} - - # Test set - Use individual hset calls for Upstash compatibility - serialized_test = self._serialize_data(test_value) - for field, value in serialized_test.items(): - self.redis.hset(test_key, field, value) - - # Test get - retrieved = self.redis.hgetall(test_key) - - # Test delete - self.redis.delete(test_key) - - return { - "status": "healthy", - "redis_connected": True, - "operations_working": bool(retrieved), - "timestamp": datetime.utcnow().isoformat() - } - - except Exception as e: - logger.error(f"Redis health check failed: {e}") - return { - "status": "unhealthy", - "redis_connected": False, - "error": str(e), - "timestamp": datetime.utcnow().isoformat() - } - - def get_stats(self) -> Dict[str, Any]: - """ - Get Redis usage statistics. - - Returns: - Statistics dictionary - """ - try: - # Get basic info (not all Upstash plans support INFO command) - stats = { - "oauth_codes_pattern": "oauth:code:*", - "sessions_pattern": "session:*", - "timestamp": datetime.utcnow().isoformat() - } - - try: - # Try to get counts (may fail on some Upstash plans) - oauth_keys = self.redis.keys("oauth:code:*") - session_keys = self.redis.keys("session:*") - - stats.update({ - "active_oauth_codes": len(oauth_keys) if oauth_keys else 0, - "active_sessions": len(session_keys) if session_keys else 0 - }) - except Exception as e: - logger.warning(f"Could not get detailed stats: {e}") - stats["warning"] = "Detailed stats not available on this Redis plan" - - return stats - - except Exception as e: - logger.error(f"Failed to get Redis stats: {e}") - return {"error": str(e), "timestamp": datetime.utcnow().isoformat()} - -# Global instance for easy importing -redis_store = None - -def get_redis_store() -> Optional[RedisSessionStore]: - """ - Get global Redis store instance (singleton pattern). - - Returns: - RedisSessionStore instance or None if initialization fails - """ - global redis_store - - if redis_store is None: - try: - logger.info("Initializing Redis store...") - redis_store = RedisSessionStore() - logger.info("Redis store initialized successfully") - except Exception as e: - logger.error(f"Failed to initialize Redis store: {e}") - redis_store = None - - return redis_store - -def init_redis_store() -> RedisSessionStore: - """ - Initialize Redis store and perform health check. - - Returns: - RedisSessionStore instance - - Raises: - Exception if Redis is not available or unhealthy - """ - store = get_redis_store() - - # Perform health check - health = store.health_check() - - if health["status"] != "healthy": - raise Exception(f"Redis health check failed: {health}") - - logger.info("Redis session store initialized and healthy") - return store \ No newline at end of file diff --git a/saidsurucu-yargi-mcp-f5fa007/rekabet_mcp_module/__init__.py b/saidsurucu-yargi-mcp-f5fa007/rekabet_mcp_module/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/saidsurucu-yargi-mcp-f5fa007/rekabet_mcp_module/client.py b/saidsurucu-yargi-mcp-f5fa007/rekabet_mcp_module/client.py deleted file mode 100644 index 1b6e782..0000000 --- a/saidsurucu-yargi-mcp-f5fa007/rekabet_mcp_module/client.py +++ /dev/null @@ -1,407 +0,0 @@ -# rekabet_mcp_module/client.py - -import httpx -from bs4 import BeautifulSoup -from typing import List, Optional, Tuple, Dict, Any -import logging -import html -import re -import io # For io.BytesIO -from urllib.parse import urlencode, urljoin, quote, parse_qs, urlparse -from markitdown import MarkItDown -import math - -# pypdf for PDF processing (lighter alternative to PyMuPDF) -from pypdf import PdfReader, PdfWriter # PyPDF2'nin devamı niteliğindeki pypdf - -from .models import ( - RekabetKurumuSearchRequest, - RekabetDecisionSummary, - RekabetSearchResult, - RekabetDocument, - RekabetKararTuruGuidEnum -) -from pydantic import HttpUrl # Ensure HttpUrl is imported from pydantic - -logger = logging.getLogger(__name__) -if not logger.hasHandlers(): # Pragma: no cover - logging.basicConfig( - level=logging.INFO, # Varsayılan log seviyesi - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' - ) - # Debug betiğinde daha detaylı loglama için seviye ayrıca ayarlanabilir. - -class RekabetKurumuApiClient: - BASE_URL = "https://www.rekabet.gov.tr" - SEARCH_PATH = "/tr/Kararlar" - DECISION_LANDING_PATH_TEMPLATE = "/Karar" - # PDF sayfa bazlı Markdown döndürüldüğü için bu sabit artık doğrudan kullanılmıyor. - # DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000 - - def __init__(self, request_timeout: float = 60.0): - self.http_client = httpx.AsyncClient( - base_url=self.BASE_URL, - headers={ - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" - }, - timeout=request_timeout, - verify=True, - follow_redirects=True - ) - - def _build_search_query_params(self, params: RekabetKurumuSearchRequest) -> List[Tuple[str, str]]: - query_params: List[Tuple[str, str]] = [] - query_params.append(("sayfaAdi", params.sayfaAdi if params.sayfaAdi is not None else "")) - query_params.append(("YayinlanmaTarihi", params.YayinlanmaTarihi if params.YayinlanmaTarihi is not None else "")) - query_params.append(("PdfText", params.PdfText if params.PdfText is not None else "")) - - karar_turu_id_value = "" - if params.KararTuruID is not None: - karar_turu_id_value = params.KararTuruID.value if params.KararTuruID.value != "ALL" else "" - query_params.append(("KararTuruID", karar_turu_id_value)) - - query_params.append(("KararSayisi", params.KararSayisi if params.KararSayisi is not None else "")) - query_params.append(("KararTarihi", params.KararTarihi if params.KararTarihi is not None else "")) - - if params.page and params.page > 1: - query_params.append(("page", str(params.page))) - - return query_params - - async def search_decisions(self, params: RekabetKurumuSearchRequest) -> RekabetSearchResult: - request_path = self.SEARCH_PATH - final_query_params = self._build_search_query_params(params) - logger.info(f"RekabetKurumuApiClient: Performing search. Path: {request_path}, Parameters: {final_query_params}") - - try: - response = await self.http_client.get(request_path, params=final_query_params) - response.raise_for_status() - html_content = response.text - except httpx.RequestError as e: - logger.error(f"RekabetKurumuApiClient: HTTP request error during search: {e}") - raise - - soup = BeautifulSoup(html_content, 'html.parser') - processed_decisions: List[RekabetDecisionSummary] = [] - total_records: Optional[int] = None - total_pages: Optional[int] = None - - pagination_div = soup.find("div", class_="yazi01") - if pagination_div: - text_content = pagination_div.get_text(separator=" ", strip=True) - total_match = re.search(r"Toplam\s*:\s*(\d+)", text_content) - if total_match: - try: - total_records = int(total_match.group(1)) - logger.debug(f"Total records found from pagination: {total_records}") - except ValueError: - logger.warning(f"Could not convert 'Toplam' value to int: {total_match.group(1)}") - else: - logger.warning("'Toplam :' string not found in pagination section.") - - results_per_page_assumed = 10 - if total_records is not None: - calculated_total_pages = math.ceil(total_records / results_per_page_assumed) - total_pages = calculated_total_pages if calculated_total_pages > 0 else (1 if total_records > 0 else 0) - logger.debug(f"Calculated total pages: {total_pages}") - - if total_pages is None: # Fallback if total_records couldn't be parsed - last_page_link = pagination_div.select_one("li.PagedList-skipToLast a") - if last_page_link and last_page_link.has_attr('href'): - qs = parse_qs(urlparse(last_page_link['href']).query) - if 'page' in qs and qs['page']: - try: - total_pages = int(qs['page'][0]) - logger.debug(f"Total pages found from 'Last >>' link: {total_pages}") - except ValueError: - logger.warning(f"Could not convert page value from 'Last >>' link to int: {qs['page'][0]}") - elif total_records == 0 : total_pages = 0 # If no records, 0 pages - elif total_records is not None and total_records > 0 : total_pages = 1 # If records exist but no last page link (e.g. single page) - else: logger.warning("'Last >>' link not found in pagination section.") - - decision_tables_container = soup.find("div", id="kararList") - if not decision_tables_container: - logger.warning("`div#kararList` (decision list container) not found. HTML structure might have changed or no decisions on this page.") - else: - decision_tables = decision_tables_container.find_all("table", class_="equalDivide") - logger.info(f"Found {len(decision_tables)} 'table' elements with class='equalDivide' for parsing.") - - if not decision_tables and total_records is not None and total_records > 0 : - logger.warning(f"Page indicates {total_records} records but no decision tables found with class='equalDivide'.") - - for idx, table in enumerate(decision_tables): - logger.debug(f"Processing table {idx + 1}...") - try: - rows = table.find_all("tr") - if len(rows) != 3: - logger.warning(f"Table {idx + 1} has an unexpected number of rows ({len(rows)} instead of 3). Skipping. HTML snippet:\n{table.prettify()[:500]}") - continue - - # Row 1: Publication Date, Decision Number, Related Cases Link - td_elements_r1 = rows[0].find_all("td") - pub_date = td_elements_r1[0].get_text(strip=True) if len(td_elements_r1) > 0 else None - dec_num = td_elements_r1[1].get_text(strip=True) if len(td_elements_r1) > 1 else None - - related_cases_link_tag = td_elements_r1[2].find("a", href=True) if len(td_elements_r1) > 2 else None - related_cases_url_str: Optional[str] = None - karar_id_from_related: Optional[str] = None - if related_cases_link_tag and related_cases_link_tag.has_attr('href'): - related_cases_url_str = urljoin(self.BASE_URL, related_cases_link_tag['href']) - qs_related = parse_qs(urlparse(related_cases_link_tag['href']).query) - if 'kararId' in qs_related and qs_related['kararId']: - karar_id_from_related = qs_related['kararId'][0] - - # Row 2: Decision Date, Decision Type - td_elements_r2 = rows[1].find_all("td") - dec_date = td_elements_r2[0].get_text(strip=True) if len(td_elements_r2) > 0 else None - dec_type_text = td_elements_r2[1].get_text(strip=True) if len(td_elements_r2) > 1 else None - - # Row 3: Title and Main Decision Link - title_cell = rows[2].find("td", colspan="5") - decision_link_tag = title_cell.find("a", href=True) if title_cell else None - - title_text: Optional[str] = None - decision_landing_url_str: Optional[str] = None - karar_id_from_main_link: Optional[str] = None - - if decision_link_tag and decision_link_tag.has_attr('href'): - title_text = decision_link_tag.get_text(strip=True) - href_val = decision_link_tag['href'] - if href_val.startswith(self.DECISION_LANDING_PATH_TEMPLATE + "?kararId="): # Ensure it's a decision link - decision_landing_url_str = urljoin(self.BASE_URL, href_val) - qs_main = parse_qs(urlparse(href_val).query) - if 'kararId' in qs_main and qs_main['kararId']: - karar_id_from_main_link = qs_main['kararId'][0] - else: - logger.warning(f"Table {idx+1} decision link has unexpected format: {href_val}") - else: - logger.warning(f"Table {idx+1} could not find title/decision link tag.") - - current_karar_id = karar_id_from_main_link or karar_id_from_related - - if not current_karar_id: - logger.warning(f"Table {idx+1} Karar ID not found. Skipping. Title (if any): {title_text}") - continue - - # Convert string URLs to HttpUrl for the model - final_decision_url = HttpUrl(decision_landing_url_str) if decision_landing_url_str else None - final_related_cases_url = HttpUrl(related_cases_url_str) if related_cases_url_str else None - - processed_decisions.append(RekabetDecisionSummary( - publication_date=pub_date, decision_number=dec_num, decision_date=dec_date, - decision_type_text=dec_type_text, title=title_text, - decision_url=final_decision_url, - karar_id=current_karar_id, - related_cases_url=final_related_cases_url - )) - logger.debug(f"Table {idx+1} parsed successfully: Karar ID '{current_karar_id}', Title '{title_text[:50] if title_text else 'N/A'}...'") - - except Exception as e: - logger.warning(f"RekabetKurumuApiClient: Error parsing decision summary {idx+1}: {e}. Problematic Table HTML:\n{table.prettify()}", exc_info=True) - continue - - return RekabetSearchResult( - decisions=processed_decisions, total_records_found=total_records, - retrieved_page_number=params.page, total_pages=total_pages if total_pages is not None else 0 - ) - - async def _extract_pdf_url_and_landing_page_metadata(self, karar_id: str, landing_page_html: str, landing_page_url: str) -> Dict[str, Any]: - soup = BeautifulSoup(landing_page_html, 'html.parser') - data: Dict[str, Any] = { - "pdf_url": None, - "title_on_landing_page": soup.title.string.strip() if soup.title and soup.title.string else f"Rekabet Kurumu Kararı {karar_id}", - } - # This part needs to be robust and specific to Rekabet Kurumu's landing page structure. - # Look for common patterns: direct links, download buttons, embedded viewers. - pdf_anchor = soup.find("a", href=re.compile(r"\.pdf(\?|$)", re.IGNORECASE)) # Basic PDF link - if not pdf_anchor: # Try other common patterns if the basic one fails - # Example: Look for links with specific text or class - pdf_anchor = soup.find("a", string=re.compile(r"karar metni|pdf indir", re.IGNORECASE)) - - if pdf_anchor and pdf_anchor.has_attr('href'): - pdf_path = pdf_anchor['href'] - data["pdf_url"] = urljoin(landing_page_url, pdf_path) - logger.info(f"PDF link found on landing page (): {data['pdf_url']}") - else: - iframe_pdf = soup.find("iframe", src=re.compile(r"\.pdf(\?|$)", re.IGNORECASE)) - if iframe_pdf and iframe_pdf.has_attr('src'): - pdf_path = iframe_pdf['src'] - data["pdf_url"] = urljoin(landing_page_url, pdf_path) - logger.info(f"PDF link found on landing page (