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
-
-[](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.
-
-
-
-🎯 **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.
-
-* Ş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 (