#
tokens: 31514/50000 1/145 files (page 10/11)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 10 of 11. Use http://codebase.md/saidsurucu/yargi-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── __main__.py
├── .dockerignore
├── .env.example
├── .gitattributes
├── .github
│   └── workflows
│       └── publish.yml
├── .gitignore
├── .serena
│   ├── .gitignore
│   └── project.yml
├── 5ire-settings.png
├── analyze_kik_hash_generation.py
├── anayasa_mcp_module
│   ├── __init__.py
│   ├── bireysel_client.py
│   ├── client.py
│   ├── models.py
│   └── unified_client.py
├── asgi_app.py
├── bddk_mcp_module
│   ├── __init__.py
│   ├── client.py
│   └── models.py
├── bedesten_mcp_module
│   ├── __init__.py
│   ├── client.py
│   ├── enums.py
│   └── models.py
├── check_response_format.py
├── CLAUDE.md
├── danistay_mcp_module
│   ├── __init__.py
│   ├── client.py
│   └── models.py
├── docker-compose.yml
├── Dockerfile
├── docs
│   └── DEPLOYMENT.md
├── emsal_mcp_module
│   ├── __init__.py
│   ├── client.py
│   └── models.py
├── example_fastapi_app.py
├── fly-no-auth.toml
├── fly.toml
├── kik_mcp_module
│   ├── __init__.py
│   ├── client_v2.py
│   ├── client.py
│   ├── models_v2.py
│   └── models.py
├── kvkk_mcp_module
│   ├── __init__.py
│   ├── client.py
│   └── models.py
├── LICENSE
├── mcp_auth
│   ├── __init__.py
│   ├── clerk_config.py
│   ├── middleware.py
│   ├── oauth.py
│   ├── policy.py
│   └── storage.py
├── mcp_auth_factory.py
├── mcp_auth_http_adapter.py
├── mcp_auth_http_simple.py
├── mcp_server_main.py
├── nginx.conf
├── ornek.png
├── Procfile
├── pyproject.toml
├── railway.json
├── README.md
├── redis_session_store.py
├── rekabet_mcp_module
│   ├── __init__.py
│   ├── client.py
│   └── models.py
├── requirements.txt
├── run_asgi.py
├── saidsurucu-yargi-mcp-f5fa007
│   ├── __main__.py
│   ├── .dockerignore
│   ├── .env.example
│   ├── .gitattributes
│   ├── .github
│   │   └── workflows
│   │       └── publish.yml
│   ├── .gitignore
│   ├── 5ire-settings.png
│   ├── anayasa_mcp_module
│   │   ├── __init__.py
│   │   ├── bireysel_client.py
│   │   ├── client.py
│   │   ├── models.py
│   │   └── unified_client.py
│   ├── asgi_app.py
│   ├── bddk_mcp_module
│   │   ├── __init__.py
│   │   ├── client.py
│   │   └── models.py
│   ├── bedesten_mcp_module
│   │   ├── __init__.py
│   │   ├── client.py
│   │   ├── enums.py
│   │   └── models.py
│   ├── check_response_format.py
│   ├── danistay_mcp_module
│   │   ├── __init__.py
│   │   ├── client.py
│   │   └── models.py
│   ├── docker-compose.yml
│   ├── Dockerfile
│   ├── docs
│   │   └── DEPLOYMENT.md
│   ├── emsal_mcp_module
│   │   ├── __init__.py
│   │   ├── client.py
│   │   └── models.py
│   ├── example_fastapi_app.py
│   ├── kik_mcp_module
│   │   ├── __init__.py
│   │   ├── client.py
│   │   └── models.py
│   ├── kvkk_mcp_module
│   │   ├── __init__.py
│   │   ├── client.py
│   │   └── models.py
│   ├── LICENSE
│   ├── mcp_auth
│   │   ├── __init__.py
│   │   ├── clerk_config.py
│   │   ├── middleware.py
│   │   ├── oauth.py
│   │   ├── policy.py
│   │   └── storage.py
│   ├── mcp_auth_factory.py
│   ├── mcp_auth_http_adapter.py
│   ├── mcp_auth_http_simple.py
│   ├── mcp_server_main.py
│   ├── nginx.conf
│   ├── ornek.png
│   ├── Procfile
│   ├── pyproject.toml
│   ├── railway.json
│   ├── README.md
│   ├── redis_session_store.py
│   ├── rekabet_mcp_module
│   │   ├── __init__.py
│   │   ├── client.py
│   │   └── models.py
│   ├── run_asgi.py
│   ├── sayistay_mcp_module
│   │   ├── __init__.py
│   │   ├── client.py
│   │   ├── enums.py
│   │   ├── models.py
│   │   └── unified_client.py
│   ├── starlette_app.py
│   ├── stripe_webhook.py
│   ├── uyusmazlik_mcp_module
│   │   ├── __init__.py
│   │   ├── client.py
│   │   └── models.py
│   └── yargitay_mcp_module
│       ├── __init__.py
│       ├── client.py
│       └── models.py
├── sayistay_mcp_module
│   ├── __init__.py
│   ├── client.py
│   ├── enums.py
│   ├── models.py
│   └── unified_client.py
├── starlette_app.py
├── stripe_webhook.py
├── uv.lock
├── uyusmazlik_mcp_module
│   ├── __init__.py
│   ├── client.py
│   └── models.py
└── yargitay_mcp_module
    ├── __init__.py
    ├── client.py
    └── models.py
```

# Files

--------------------------------------------------------------------------------
/mcp_server_main.py:
--------------------------------------------------------------------------------

```python
   1 | # mcp_server_main.py
   2 | import asyncio
   3 | import atexit
   4 | import logging
   5 | import os
   6 | import httpx
   7 | import json
   8 | import time
   9 | from collections import defaultdict
  10 | from pydantic import BaseModel, HttpUrl, Field 
  11 | from typing import Optional, Dict, List, Literal, Any, Union
  12 | import urllib.parse
  13 | from fastmcp.server.middleware import Middleware, MiddlewareContext
  14 | 
  15 | # Optional tiktoken import for token counting
  16 | try:
  17 |     import tiktoken
  18 |     TIKTOKEN_AVAILABLE = True
  19 | except ImportError:
  20 |     TIKTOKEN_AVAILABLE = False
  21 |     tiktoken = None
  22 | from fastmcp.server.dependencies import get_access_token, AccessToken
  23 | from fastmcp import Context
  24 | 
  25 | # Use standard exception for tool errors
  26 | class ToolError(Exception):
  27 |     """Tool execution error"""
  28 |     pass
  29 | 
  30 | # --- Logging Configuration Start ---
  31 | root_logger = logging.getLogger()
  32 | root_logger.setLevel(logging.INFO)
  33 | 
  34 | console_handler = logging.StreamHandler()
  35 | log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  36 | console_handler.setFormatter(log_formatter)
  37 | console_handler.setLevel(logging.INFO)
  38 | root_logger.addHandler(console_handler)
  39 | 
  40 | logger = logging.getLogger(__name__)
  41 | # --- Logging Configuration End ---
  42 | 
  43 | # --- Token Counting Middleware ---
  44 | class TokenCountingMiddleware(Middleware):
  45 |     """Middleware for counting input/output tokens using tiktoken."""
  46 |     
  47 |     def __init__(self, model: str = "cl100k_base"):
  48 |         """Initialize token counting middleware.
  49 | 
  50 |         Args:
  51 |             model: Tiktoken model name (cl100k_base for GPT-4/Claude compatibility)
  52 |         """
  53 |         if not TIKTOKEN_AVAILABLE:
  54 |             raise ImportError("tiktoken is required for token counting. Install with: pip install tiktoken")
  55 | 
  56 |         self.encoder = tiktoken.get_encoding(model)
  57 |         self.model = model
  58 |         self.token_stats = defaultdict(lambda: {"input": 0, "output": 0, "calls": 0})
  59 |         self.logger = logging.getLogger("token_counter")
  60 |         self.logger.setLevel(logging.INFO)
  61 |     
  62 |     def count_tokens(self, text: str) -> int:
  63 |         """Count tokens in text using tiktoken."""
  64 |         if not text:
  65 |             return 0
  66 |         try:
  67 |             return len(self.encoder.encode(str(text)))
  68 |         except Exception as e:
  69 |             logger.warning(f"Token counting failed: {e}")
  70 |             return 0
  71 |     
  72 |     def extract_text_content(self, data: Any) -> str:
  73 |         """Extract text content from various data types."""
  74 |         if isinstance(data, str):
  75 |             return data
  76 |         elif isinstance(data, dict):
  77 |             # Extract text from common response fields
  78 |             text_parts = []
  79 |             for key, value in data.items():
  80 |                 if isinstance(value, str):
  81 |                     text_parts.append(value)
  82 |                 elif isinstance(value, list):
  83 |                     for item in value:
  84 |                         if isinstance(item, str):
  85 |                             text_parts.append(item)
  86 |                         elif isinstance(item, dict) and 'text' in item:
  87 |                             text_parts.append(str(item['text']))
  88 |             return ' '.join(text_parts)
  89 |         elif isinstance(data, list):
  90 |             text_parts = []
  91 |             for item in data:
  92 |                 text_parts.append(self.extract_text_content(item))
  93 |             return ' '.join(text_parts)
  94 |         else:
  95 |             return str(data)
  96 |     
  97 |     def log_token_usage(self, operation: str, input_tokens: int, output_tokens: int, 
  98 |                        tool_name: str = None, duration_ms: float = None):
  99 |         """Log token usage with structured format."""
 100 |         log_data = {
 101 |             "operation": operation,
 102 |             "tool_name": tool_name,
 103 |             "input_tokens": input_tokens,
 104 |             "output_tokens": output_tokens,
 105 |             "total_tokens": input_tokens + output_tokens,
 106 |             "duration_ms": duration_ms,
 107 |             "timestamp": time.time()
 108 |         }
 109 |         
 110 |         # Update statistics
 111 |         key = tool_name if tool_name else operation
 112 |         self.token_stats[key]["input"] += input_tokens
 113 |         self.token_stats[key]["output"] += output_tokens
 114 |         self.token_stats[key]["calls"] += 1
 115 |         
 116 |         # Log as JSON for easy parsing
 117 |         self.logger.info(json.dumps(log_data))
 118 |         
 119 |         # Also log human-readable format to main logger
 120 |         logger.info(f"Token Usage - {operation}" + 
 121 |                    (f" ({tool_name})" if tool_name else "") +
 122 |                    f": {input_tokens} in + {output_tokens} out = {input_tokens + output_tokens} total")
 123 |     
 124 |     async def on_call_tool(self, context: MiddlewareContext, call_next):
 125 |         """Count tokens for tool calls."""
 126 |         start_time = time.perf_counter()
 127 |         
 128 |         # Extract tool name and arguments
 129 |         tool_name = getattr(context.message, 'name', 'unknown_tool')
 130 |         tool_args = getattr(context.message, 'arguments', {})
 131 |         
 132 |         # Count input tokens (tool arguments)
 133 |         input_text = self.extract_text_content(tool_args)
 134 |         input_tokens = self.count_tokens(input_text)
 135 |         
 136 |         try:
 137 |             # Execute the tool
 138 |             result = await call_next(context)
 139 |             
 140 |             # Count output tokens (tool result)
 141 |             output_text = self.extract_text_content(result)
 142 |             output_tokens = self.count_tokens(output_text)
 143 |             
 144 |             # Calculate duration
 145 |             duration_ms = (time.perf_counter() - start_time) * 1000
 146 |             
 147 |             # Log token usage
 148 |             self.log_token_usage("tool_call", input_tokens, output_tokens, 
 149 |                                tool_name, duration_ms)
 150 |             
 151 |             return result
 152 |             
 153 |         except Exception as e:
 154 |             duration_ms = (time.perf_counter() - start_time) * 1000
 155 |             self.log_token_usage("tool_call_error", input_tokens, 0, 
 156 |                                tool_name, duration_ms)
 157 |             raise
 158 |     
 159 |     async def on_read_resource(self, context: MiddlewareContext, call_next):
 160 |         """Count tokens for resource reads."""
 161 |         start_time = time.perf_counter()
 162 |         
 163 |         # Extract resource URI
 164 |         resource_uri = getattr(context.message, 'uri', 'unknown_resource')
 165 |         
 166 |         try:
 167 |             # Execute the resource read
 168 |             result = await call_next(context)
 169 |             
 170 |             # Count output tokens (resource content)
 171 |             output_text = self.extract_text_content(result)
 172 |             output_tokens = self.count_tokens(output_text)
 173 |             
 174 |             # Calculate duration
 175 |             duration_ms = (time.perf_counter() - start_time) * 1000
 176 |             
 177 |             # Log token usage (no input tokens for resource reads)
 178 |             self.log_token_usage("resource_read", 0, output_tokens, 
 179 |                                resource_uri, duration_ms)
 180 |             
 181 |             return result
 182 |             
 183 |         except Exception as e:
 184 |             duration_ms = (time.perf_counter() - start_time) * 1000
 185 |             self.log_token_usage("resource_read_error", 0, 0, 
 186 |                                resource_uri, duration_ms)
 187 |             raise
 188 |     
 189 |     async def on_get_prompt(self, context: MiddlewareContext, call_next):
 190 |         """Count tokens for prompt retrievals."""
 191 |         start_time = time.perf_counter()
 192 |         
 193 |         # Extract prompt name
 194 |         prompt_name = getattr(context.message, 'name', 'unknown_prompt')
 195 |         
 196 |         try:
 197 |             # Execute the prompt retrieval
 198 |             result = await call_next(context)
 199 |             
 200 |             # Count output tokens (prompt content)
 201 |             output_text = self.extract_text_content(result)
 202 |             output_tokens = self.count_tokens(output_text)
 203 |             
 204 |             # Calculate duration
 205 |             duration_ms = (time.perf_counter() - start_time) * 1000
 206 |             
 207 |             # Log token usage
 208 |             self.log_token_usage("prompt_get", 0, output_tokens, 
 209 |                                prompt_name, duration_ms)
 210 |             
 211 |             return result
 212 |             
 213 |         except Exception as e:
 214 |             duration_ms = (time.perf_counter() - start_time) * 1000
 215 |             self.log_token_usage("prompt_get_error", 0, 0, 
 216 |                                prompt_name, duration_ms)
 217 |             raise
 218 |     
 219 |     def get_token_stats(self) -> Dict[str, Any]:
 220 |         """Get current token usage statistics."""
 221 |         return dict(self.token_stats)
 222 |     
 223 |     def reset_token_stats(self):
 224 |         """Reset token usage statistics."""
 225 |         self.token_stats.clear()
 226 | 
 227 | # --- End Token Counting Middleware ---
 228 | 
 229 | # Create FastMCP app directly without authentication wrapper
 230 | from fastmcp import FastMCP
 231 | 
 232 | def create_app(auth=None):
 233 |     """Create FastMCP app with standard capabilities and optional auth."""
 234 |     global app
 235 |     if auth:
 236 |         app.auth = auth
 237 |         logger.info("MCP server created with Bearer authentication enabled")
 238 |     else:
 239 |         logger.info("MCP server created with standard capabilities...")
 240 |     
 241 |     # Add token counting middleware only if tiktoken is available
 242 |     if TIKTOKEN_AVAILABLE:
 243 |         try:
 244 |             token_counter = TokenCountingMiddleware()
 245 |             app.add_middleware(token_counter)
 246 |             logger.info("Token counting middleware added to MCP server")
 247 |         except Exception as e:
 248 |             logger.warning(f"Failed to add token counting middleware: {e}")
 249 |     
 250 |     return app
 251 | 
 252 | # --- Module Imports ---
 253 | from yargitay_mcp_module.client import YargitayOfficialApiClient
 254 | from yargitay_mcp_module.models import (
 255 |     YargitayDetailedSearchRequest, YargitayDocumentMarkdown, CompactYargitaySearchResult,
 256 |     YargitayBirimEnum, CleanYargitayDecisionEntry
 257 | )
 258 | from bedesten_mcp_module.client import BedestenApiClient
 259 | from bedesten_mcp_module.models import (
 260 |     BedestenSearchRequest, BedestenSearchData,
 261 |     BedestenDocumentMarkdown, BedestenCourtTypeEnum
 262 | )
 263 | from bedesten_mcp_module.enums import BirimAdiEnum
 264 | from danistay_mcp_module.client import DanistayApiClient
 265 | from danistay_mcp_module.models import (
 266 |     DanistayKeywordSearchRequest, DanistayDetailedSearchRequest,
 267 |     DanistayDocumentMarkdown, CompactDanistaySearchResult
 268 | )
 269 | from emsal_mcp_module.client import EmsalApiClient
 270 | from emsal_mcp_module.models import (
 271 |     EmsalSearchRequest, EmsalDocumentMarkdown, CompactEmsalSearchResult
 272 | )
 273 | from uyusmazlik_mcp_module.client import UyusmazlikApiClient
 274 | from uyusmazlik_mcp_module.models import (
 275 |     UyusmazlikSearchRequest, UyusmazlikSearchResponse, UyusmazlikDocumentMarkdown,
 276 |     UyusmazlikBolumEnum, UyusmazlikTuruEnum, UyusmazlikKararSonucuEnum
 277 | )
 278 | from anayasa_mcp_module.client import AnayasaMahkemesiApiClient
 279 | from anayasa_mcp_module.bireysel_client import AnayasaBireyselBasvuruApiClient
 280 | from anayasa_mcp_module.unified_client import AnayasaUnifiedClient
 281 | from anayasa_mcp_module.models import (
 282 |     AnayasaNormDenetimiSearchRequest,
 283 |     AnayasaSearchResult,
 284 |     AnayasaDocumentMarkdown,
 285 |     AnayasaBireyselReportSearchRequest,
 286 |     AnayasaBireyselReportSearchResult,
 287 |     AnayasaBireyselBasvuruDocumentMarkdown,
 288 |     AnayasaUnifiedSearchRequest,
 289 |     AnayasaUnifiedSearchResult,
 290 |     AnayasaUnifiedDocumentMarkdown,
 291 |     # Removed enum imports - now using Literal strings in models
 292 | )
 293 | # KIK v2 Module Imports (New API)
 294 | from kik_mcp_module.client_v2 import KikV2ApiClient
 295 | from kik_mcp_module.models_v2 import KikV2DecisionType
 296 | from kik_mcp_module.models_v2 import (
 297 |     KikV2SearchResult,
 298 |     KikV2DocumentMarkdown
 299 | )
 300 | 
 301 | from rekabet_mcp_module.client import RekabetKurumuApiClient
 302 | from rekabet_mcp_module.models import (
 303 |     RekabetKurumuSearchRequest,
 304 |     RekabetSearchResult,
 305 |     RekabetDocument,
 306 |     RekabetKararTuruGuidEnum
 307 | )
 308 | 
 309 | from sayistay_mcp_module.client import SayistayApiClient
 310 | from sayistay_mcp_module.models import (
 311 |     GenelKurulSearchRequest, GenelKurulSearchResponse,
 312 |     TemyizKuruluSearchRequest, TemyizKuruluSearchResponse,
 313 |     DaireSearchRequest, DaireSearchResponse,
 314 |     SayistayDocumentMarkdown,
 315 |     SayistayUnifiedSearchRequest, SayistayUnifiedSearchResult,
 316 |     SayistayUnifiedDocumentMarkdown
 317 | )
 318 | from sayistay_mcp_module.enums import DaireEnum, KamuIdaresiTuruEnum, WebKararKonusuEnum
 319 | from sayistay_mcp_module.unified_client import SayistayUnifiedClient
 320 | 
 321 | # KVKK Module Imports
 322 | from kvkk_mcp_module.client import KvkkApiClient
 323 | from kvkk_mcp_module.models import (
 324 |     KvkkSearchRequest,
 325 |     KvkkSearchResult,
 326 |     KvkkDocumentMarkdown
 327 | )
 328 | 
 329 | # BDDK Module Imports
 330 | from bddk_mcp_module.client import BddkApiClient
 331 | from bddk_mcp_module.models import (
 332 |     BddkSearchRequest,
 333 |     BddkSearchResult,
 334 |     BddkDocumentMarkdown
 335 | )
 336 | 
 337 | 
 338 | # Create a placeholder app that will be properly initialized after tools are defined
 339 | from fastmcp import FastMCP
 340 | 
 341 | # MCP app for Turkish legal databases with explicit capabilities
 342 | app = FastMCP(
 343 |     name="Yargı MCP Server",
 344 |     version="0.1.6"
 345 | )
 346 | 
 347 | # --- Health Check Functions (using individual clients) ---
 348 | 
 349 | # --- API Client Instances ---
 350 | yargitay_client_instance = YargitayOfficialApiClient()
 351 | danistay_client_instance = DanistayApiClient()
 352 | emsal_client_instance = EmsalApiClient()
 353 | uyusmazlik_client_instance = UyusmazlikApiClient()
 354 | anayasa_norm_client_instance = AnayasaMahkemesiApiClient()
 355 | anayasa_bireysel_client_instance = AnayasaBireyselBasvuruApiClient()
 356 | anayasa_unified_client_instance = AnayasaUnifiedClient()
 357 | kik_v2_client_instance = KikV2ApiClient()
 358 | rekabet_client_instance = RekabetKurumuApiClient()
 359 | bedesten_client_instance = BedestenApiClient()
 360 | sayistay_client_instance = SayistayApiClient()
 361 | sayistay_unified_client_instance = SayistayUnifiedClient()
 362 | kvkk_client_instance = KvkkApiClient()
 363 | bddk_client_instance = BddkApiClient()
 364 | 
 365 | 
 366 | KARAR_TURU_ADI_TO_GUID_ENUM_MAP = {
 367 |     "": RekabetKararTuruGuidEnum.TUMU,  # Keep for backward compatibility
 368 |     "ALL": RekabetKararTuruGuidEnum.TUMU,  # Map "ALL" to TUMU
 369 |     "Birleşme ve Devralma": RekabetKararTuruGuidEnum.BIRLESME_DEVRALMA,
 370 |     "Diğer": RekabetKararTuruGuidEnum.DIGER,
 371 |     "Menfi Tespit ve Muafiyet": RekabetKararTuruGuidEnum.MENFI_TESPIT_MUAFIYET,
 372 |     "Özelleştirme": RekabetKararTuruGuidEnum.OZELLESTIRME,
 373 |     "Rekabet İhlali": RekabetKararTuruGuidEnum.REKABET_IHLALI,
 374 | }
 375 | 
 376 | # --- MCP Tools for Yargitay ---
 377 | """
 378 | @app.tool(
 379 |     description="Search Yargıtay decisions with 52 chamber filtering and advanced operators",
 380 |     annotations={
 381 |         "readOnlyHint": True,
 382 |         "openWorldHint": True,
 383 |         "idempotentHint": True
 384 |     }
 385 | )
 386 | async def search_yargitay_detailed(
 387 |     arananKelime: str = Field("", description="Turkish search keyword. Supports +required -excluded \"exact phrase\" operators"),
 388 |     birimYrgKurulDaire: str = Field("ALL", description="Chamber selection (52 options: Civil/Criminal chambers, General Assemblies)"),
 389 |     esasYil: str = Field("", description="Case year for 'Esas No'."),
 390 |     esasIlkSiraNo: str = Field("", description="Starting sequence number for 'Esas No'."),
 391 |     esasSonSiraNo: str = Field("", description="Ending sequence number for 'Esas No'."),
 392 |     kararYil: str = Field("", description="Decision year for 'Karar No'."),
 393 |     kararIlkSiraNo: str = Field("", description="Starting sequence number for 'Karar No'."),
 394 |     kararSonSiraNo: str = Field("", description="Ending sequence number for 'Karar No'."),
 395 |     baslangicTarihi: str = Field("", description="Start date for decision search (DD.MM.YYYY)."),
 396 |     bitisTarihi: str = Field("", description="End date for decision search (DD.MM.YYYY)."),
 397 |     # pageSize: int = Field(10, ge=1, le=10, description="Number of results per page."),
 398 |     pageNumber: int = Field(1, ge=1, description="Page number to retrieve.")
 399 | ) -> CompactYargitaySearchResult:
 400 |     # Search Yargıtay decisions using primary API with 52 chamber filtering and advanced operators.
 401 |     
 402 |     # Convert "ALL" to empty string for API compatibility
 403 |     if birimYrgKurulDaire == "ALL":
 404 |         birimYrgKurulDaire = ""
 405 |     
 406 |     pageSize = 10  # Default value
 407 |     
 408 |     search_query = YargitayDetailedSearchRequest(
 409 |         arananKelime=arananKelime,
 410 |         birimYrgKurulDaire=birimYrgKurulDaire,
 411 |         esasYil=esasYil,
 412 |         esasIlkSiraNo=esasIlkSiraNo,
 413 |         esasSonSiraNo=esasSonSiraNo,
 414 |         kararYil=kararYil,
 415 |         kararIlkSiraNo=kararIlkSiraNo,
 416 |         kararSonSiraNo=kararSonSiraNo,
 417 |         baslangicTarihi=baslangicTarihi,
 418 |         bitisTarihi=bitisTarihi,
 419 |         siralama="3",
 420 |         siralamaDirection="desc",
 421 |         pageSize=pageSize,
 422 |         pageNumber=pageNumber
 423 |     )
 424 |     
 425 |     logger.info(f"Tool 'search_yargitay_detailed' called: {search_query.model_dump_json(exclude_none=True, indent=2)}")
 426 |     try:
 427 |         api_response = await yargitay_client_instance.search_detailed_decisions(search_query)
 428 |         if api_response and api_response.data and api_response.data.data:
 429 |             # Convert to clean decision entries without arananKelime field
 430 |             clean_decisions = [
 431 |                 CleanYargitayDecisionEntry(
 432 |                     id=decision.id,
 433 |                     daire=decision.daire,
 434 |                     esasNo=decision.esasNo,
 435 |                     kararNo=decision.kararNo,
 436 |                     kararTarihi=decision.kararTarihi,
 437 |                     document_url=decision.document_url
 438 |                 )
 439 |                 for decision in api_response.data.data
 440 |             ]
 441 |             return CompactYargitaySearchResult(
 442 |                 decisions=clean_decisions,
 443 |                 total_records=api_response.data.recordsTotal if api_response.data else 0,
 444 |                 requested_page=search_query.pageNumber,
 445 |                 page_size=search_query.pageSize)
 446 |         logger.warning("API response for Yargitay search did not contain expected data structure.")
 447 |         return CompactYargitaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
 448 |     except Exception as e:
 449 |         logger.exception(f"Error in tool 'search_yargitay_detailed'.")
 450 |         raise
 451 | 
 452 | @app.tool(
 453 |     description="Get Yargıtay decision text in Markdown format",
 454 |     annotations={
 455 |         "readOnlyHint": True,
 456 |         "idempotentHint": True
 457 |     }
 458 | )
 459 | async def get_yargitay_document_markdown(id: str) -> YargitayDocumentMarkdown:
 460 |     # Get Yargıtay decision text as Markdown. Use ID from search results.
 461 |     logger.info(f"Tool 'get_yargitay_document_markdown' called for ID: {id}")
 462 |     if not id or not id.strip(): raise ValueError("Document ID must be a non-empty string.")
 463 |     try:
 464 |         return await yargitay_client_instance.get_decision_document_as_markdown(id)
 465 |     except Exception as e:
 466 |         logger.exception(f"Error in tool 'get_yargitay_document_markdown'.")
 467 |         raise
 468 | """
 469 | 
 470 | # --- MCP Tools for Danistay ---
 471 | """
 472 | @app.tool(
 473 |     description="Search Danıştay decisions with keyword logic (AND/OR/NOT operators)",
 474 |     annotations={
 475 |         "readOnlyHint": True,
 476 |         "openWorldHint": True,
 477 |         "idempotentHint": True
 478 |     }
 479 | )
 480 | async def search_danistay_by_keyword(
 481 |     andKelimeler: List[str] = Field(default_factory=list, description="Keywords for AND logic, e.g., ['word1', 'word2']"),
 482 |     orKelimeler: List[str] = Field(default_factory=list, description="Keywords for OR logic."),
 483 |     notAndKelimeler: List[str] = Field(default_factory=list, description="Keywords for NOT AND logic."),
 484 |     notOrKelimeler: List[str] = Field(default_factory=list, description="Keywords for NOT OR logic."),
 485 |     pageNumber: int = Field(1, ge=1, description="Page number."),
 486 |     # pageSize: int = Field(10, ge=1, le=10, description="Results per page.")
 487 | ) -> CompactDanistaySearchResult:
 488 |     # Search Danıştay decisions with keyword logic.
 489 |     
 490 |     pageSize = 10  # Default value
 491 |     
 492 |     search_query = DanistayKeywordSearchRequest(
 493 |         andKelimeler=andKelimeler,
 494 |         orKelimeler=orKelimeler,
 495 |         notAndKelimeler=notAndKelimeler,
 496 |         notOrKelimeler=notOrKelimeler,
 497 |         pageNumber=pageNumber,
 498 |         pageSize=pageSize
 499 |     )
 500 |     
 501 |     logger.info(f"Tool 'search_danistay_by_keyword' called.")
 502 |     try:
 503 |         api_response = await danistay_client_instance.search_keyword_decisions(search_query)
 504 |         if api_response.data:
 505 |             return CompactDanistaySearchResult(
 506 |                 decisions=api_response.data.data,
 507 |                 total_records=api_response.data.recordsTotal,
 508 |                 requested_page=search_query.pageNumber,
 509 |                 page_size=search_query.pageSize)
 510 |         logger.warning("API response for Danistay keyword search did not contain expected data structure.")
 511 |         return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
 512 |     except Exception as e:
 513 |         logger.exception(f"Error in tool 'search_danistay_by_keyword'.")
 514 |         raise
 515 | 
 516 | @app.tool(
 517 |     description="Search Danıştay decisions with detailed criteria (chamber selection, case numbers)",
 518 |     annotations={
 519 |         "readOnlyHint": True,
 520 |         "openWorldHint": True,
 521 |         "idempotentHint": True
 522 |     }
 523 | )
 524 | async def search_danistay_detailed(
 525 |     daire: str = Field("", description="Chamber/Department name (e.g., '1. Daire')."),
 526 |     esasYil: str = Field("", description="Case year for 'Esas No'."),
 527 |     esasIlkSiraNo: str = Field("", description="Starting sequence for 'Esas No'."),
 528 |     esasSonSiraNo: str = Field("", description="Ending sequence for 'Esas No'."),
 529 |     kararYil: str = Field("", description="Decision year for 'Karar No'."),
 530 |     kararIlkSiraNo: str = Field("", description="Starting sequence for 'Karar No'."),
 531 |     kararSonSiraNo: str = Field("", description="Ending sequence for 'Karar No'."),
 532 |     baslangicTarihi: str = Field("", description="Start date for decision (DD.MM.YYYY)."),
 533 |     bitisTarihi: str = Field("", description="End date for decision (DD.MM.YYYY)."),
 534 |     mevzuatNumarasi: str = Field("", description="Legislation number."),
 535 |     mevzuatAdi: str = Field("", description="Legislation name."),
 536 |     madde: str = Field("", description="Article number."),
 537 |     pageNumber: int = Field(1, ge=1, description="Page number."),
 538 |     # pageSize: int = Field(10, ge=1, le=10, description="Results per page.")
 539 | ) -> CompactDanistaySearchResult:
 540 |     # Search Danıştay decisions with detailed filtering.
 541 |     
 542 |     pageSize = 10  # Default value
 543 |     
 544 |     search_query = DanistayDetailedSearchRequest(
 545 |         daire=daire,
 546 |         esasYil=esasYil,
 547 |         esasIlkSiraNo=esasIlkSiraNo,
 548 |         esasSonSiraNo=esasSonSiraNo,
 549 |         kararYil=kararYil,
 550 |         kararIlkSiraNo=kararIlkSiraNo,
 551 |         kararSonSiraNo=kararSonSiraNo,
 552 |         baslangicTarihi=baslangicTarihi,
 553 |         bitisTarihi=bitisTarihi,
 554 |         mevzuatNumarasi=mevzuatNumarasi,
 555 |         mevzuatAdi=mevzuatAdi,
 556 |         madde=madde,
 557 |         siralama="3",
 558 |         siralamaDirection="desc",
 559 |         pageNumber=pageNumber,
 560 |         pageSize=pageSize
 561 |     )
 562 |     
 563 |     logger.info(f"Tool 'search_danistay_detailed' called.")
 564 |     try:
 565 |         api_response = await danistay_client_instance.search_detailed_decisions(search_query)
 566 |         if api_response.data:
 567 |             return CompactDanistaySearchResult(
 568 |                 decisions=api_response.data.data,
 569 |                 total_records=api_response.data.recordsTotal,
 570 |                 requested_page=search_query.pageNumber,
 571 |                 page_size=search_query.pageSize)
 572 |         logger.warning("API response for Danistay detailed search did not contain expected data structure.")
 573 |         return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
 574 |     except Exception as e:
 575 |         logger.exception(f"Error in tool 'search_danistay_detailed'.")
 576 |         raise
 577 | 
 578 | @app.tool(
 579 |     description="Get Danıştay decision text in Markdown format",
 580 |     annotations={
 581 |         "readOnlyHint": True,
 582 |         "idempotentHint": True
 583 |     }
 584 | )
 585 | async def get_danistay_document_markdown(id: str) -> DanistayDocumentMarkdown:
 586 |     # Get Danıştay decision text as Markdown. Use ID from search results.
 587 |     logger.info(f"Tool 'get_danistay_document_markdown' called for ID: {id}")
 588 |     if not id or not id.strip(): raise ValueError("Document ID must be a non-empty string for Danıştay.")
 589 |     try:
 590 |         return await danistay_client_instance.get_decision_document_as_markdown(id)
 591 |     except Exception as e:
 592 |         logger.exception(f"Error in tool 'get_danistay_document_markdown'.")
 593 |         raise
 594 | """
 595 | 
 596 | # --- MCP Tools for Emsal ---
 597 | @app.tool(
 598 |     description="Search Emsal precedent decisions with detailed criteria",
 599 |     annotations={
 600 |         "readOnlyHint": True,
 601 |         "openWorldHint": True,
 602 |         "idempotentHint": True
 603 |     }
 604 | )
 605 | async def search_emsal_detailed_decisions(
 606 |     keyword: str = Field("", description="Keyword to search."),
 607 |     selected_bam_civil_court: str = Field("", description="Selected BAM Civil Court."),
 608 |     selected_civil_court: str = Field("", description="Selected Civil Court."),
 609 |     selected_regional_civil_chambers: List[str] = Field(default_factory=list, description="Selected Regional Civil Chambers."),
 610 |     case_year_esas: str = Field("", description="Case year for 'Esas No'."),
 611 |     case_start_seq_esas: str = Field("", description="Starting sequence for 'Esas No'."),
 612 |     case_end_seq_esas: str = Field("", description="Ending sequence for 'Esas No'."),
 613 |     decision_year_karar: str = Field("", description="Decision year for 'Karar No'."),
 614 |     decision_start_seq_karar: str = Field("", description="Starting sequence for 'Karar No'."),
 615 |     decision_end_seq_karar: str = Field("", description="Ending sequence for 'Karar No'."),
 616 |     start_date: str = Field("", description="Start date for decision (DD.MM.YYYY)."),
 617 |     end_date: str = Field("", description="End date for decision (DD.MM.YYYY)."),
 618 |     sort_criteria: str = Field("1", description="Sorting criteria (e.g., 1: Esas No)."),
 619 |     sort_direction: str = Field("desc", description="Sorting direction ('asc' or 'desc')."),
 620 |     page_number: int = Field(1, ge=1, description="Page number (accepts int)."),
 621 |     # page_size: int = Field(10, ge=1, le=10, description="Results per page.")
 622 | ) -> Dict[str, Any]:
 623 |     """Search Emsal precedent decisions with detailed criteria."""
 624 |     
 625 |     page_size = 10  # Default value
 626 |     
 627 |     search_query = EmsalSearchRequest(
 628 |         keyword=keyword,
 629 |         selected_bam_civil_court=selected_bam_civil_court,
 630 |         selected_civil_court=selected_civil_court,
 631 |         selected_regional_civil_chambers=selected_regional_civil_chambers,
 632 |         case_year_esas=case_year_esas,
 633 |         case_start_seq_esas=case_start_seq_esas,
 634 |         case_end_seq_esas=case_end_seq_esas,
 635 |         decision_year_karar=decision_year_karar,
 636 |         decision_start_seq_karar=decision_start_seq_karar,
 637 |         decision_end_seq_karar=decision_end_seq_karar,
 638 |         start_date=start_date,
 639 |         end_date=end_date,
 640 |         sort_criteria=sort_criteria,
 641 |         sort_direction=sort_direction,
 642 |         page_number=page_number,
 643 |         page_size=page_size
 644 |     )
 645 |     
 646 |     logger.info(f"Tool 'search_emsal_detailed_decisions' called.")
 647 |     try:
 648 |         api_response = await emsal_client_instance.search_detailed_decisions(search_query)
 649 |         if api_response.data:
 650 |             return CompactEmsalSearchResult(
 651 |                 decisions=api_response.data.data,
 652 |                 total_records=api_response.data.recordsTotal if api_response.data.recordsTotal is not None else 0,
 653 |                 requested_page=search_query.page_number,
 654 |                 page_size=search_query.page_size
 655 |             ).model_dump()
 656 |         logger.warning("API response for Emsal search did not contain expected data structure.")
 657 |         return CompactEmsalSearchResult(decisions=[], total_records=0, requested_page=search_query.page_number, page_size=search_query.page_size).model_dump()
 658 |     except Exception as e:
 659 |         logger.exception(f"Error in tool 'search_emsal_detailed_decisions'.")
 660 |         raise
 661 | 
 662 | @app.tool(
 663 |     description="Get Emsal precedent decision text in Markdown format",
 664 |     annotations={
 665 |         "readOnlyHint": True,
 666 |         "idempotentHint": True
 667 |     }
 668 | )
 669 | async def get_emsal_document_markdown(id: str) -> Dict[str, Any]:
 670 |     """Get document as Markdown."""
 671 |     logger.info(f"Tool 'get_emsal_document_markdown' called for ID: {id}")
 672 |     if not id or not id.strip(): raise ValueError("Document ID required for Emsal.")
 673 |     try:
 674 |         result = await emsal_client_instance.get_decision_document_as_markdown(id)
 675 |         return result.model_dump()
 676 |     except Exception as e:
 677 |         logger.exception(f"Error in tool 'get_emsal_document_markdown'.")
 678 |         raise
 679 | 
 680 | # --- MCP Tools for Uyusmazlik ---
 681 | @app.tool(
 682 |     description="Search Uyuşmazlık Mahkemesi decisions for jurisdictional disputes",
 683 |     annotations={
 684 |         "readOnlyHint": True,
 685 |         "openWorldHint": True,
 686 |         "idempotentHint": True
 687 |     }
 688 | )
 689 | async def search_uyusmazlik_decisions(
 690 |     icerik: str = Field("", description="Keyword or content for main text search."),
 691 |     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."),
 692 |     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."),
 693 |     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."),
 694 |     esas_yil: str = Field("", description="Case year ('Esas Yılı')."),
 695 |     esas_sayisi: str = Field("", description="Case number ('Esas Sayısı')."),
 696 |     karar_yil: str = Field("", description="Decision year ('Karar Yılı')."),
 697 |     karar_sayisi: str = Field("", description="Decision number ('Karar Sayısı')."),
 698 |     kanun_no: str = Field("", description="Relevant Law Number."),
 699 |     karar_date_begin: str = Field("", description="Decision start date (DD.MM.YYYY)."),
 700 |     karar_date_end: str = Field("", description="Decision end date (DD.MM.YYYY)."),
 701 |     resmi_gazete_sayi: str = Field("", description="Official Gazette number."),
 702 |     resmi_gazete_date: str = Field("", description="Official Gazette date (DD.MM.YYYY)."),
 703 |     tumce: str = Field("", description="Exact phrase search."),
 704 |     wild_card: str = Field("", description="Search for phrase and its inflections."),
 705 |     hepsi: str = Field("", description="Search for texts containing all specified words."),
 706 |     herhangi_birisi: str = Field("", description="Search for texts containing any of the specified words."),
 707 |     not_hepsi: str = Field("", description="Exclude texts containing these specified words.")
 708 | ) -> Dict[str, Any]:
 709 |     """Search Court of Jurisdictional Disputes decisions."""
 710 |     
 711 |     # Convert string literals to enums
 712 |     # Map "ALL" to TUMU for backward compatibility
 713 |     if bolum == "ALL":
 714 |         bolum_enum = UyusmazlikBolumEnum.TUMU
 715 |     else:
 716 |         bolum_enum = UyusmazlikBolumEnum(bolum) if bolum else UyusmazlikBolumEnum.TUMU
 717 |     
 718 |     if uyusmazlik_turu == "ALL":
 719 |         uyusmazlik_turu_enum = UyusmazlikTuruEnum.TUMU
 720 |     else:
 721 |         uyusmazlik_turu_enum = UyusmazlikTuruEnum(uyusmazlik_turu) if uyusmazlik_turu else UyusmazlikTuruEnum.TUMU
 722 |     karar_sonuclari_enums = [UyusmazlikKararSonucuEnum(ks) for ks in karar_sonuclari]
 723 |     
 724 |     search_params = UyusmazlikSearchRequest(
 725 |         icerik=icerik,
 726 |         bolum=bolum_enum,
 727 |         uyusmazlik_turu=uyusmazlik_turu_enum,
 728 |         karar_sonuclari=karar_sonuclari_enums,
 729 |         esas_yil=esas_yil,
 730 |         esas_sayisi=esas_sayisi,
 731 |         karar_yil=karar_yil,
 732 |         karar_sayisi=karar_sayisi,
 733 |         kanun_no=kanun_no,
 734 |         karar_date_begin=karar_date_begin,
 735 |         karar_date_end=karar_date_end,
 736 |         resmi_gazete_sayi=resmi_gazete_sayi,
 737 |         resmi_gazete_date=resmi_gazete_date,
 738 |         tumce=tumce,
 739 |         wild_card=wild_card,
 740 |         hepsi=hepsi,
 741 |         herhangi_birisi=herhangi_birisi,
 742 |         not_hepsi=not_hepsi
 743 |     )
 744 |     
 745 |     logger.info(f"Tool 'search_uyusmazlik_decisions' called.")
 746 |     try:
 747 |         result = await uyusmazlik_client_instance.search_decisions(search_params)
 748 |         return result.model_dump()
 749 |     except Exception as e:
 750 |         logger.exception(f"Error in tool 'search_uyusmazlik_decisions'.")
 751 |         raise
 752 | 
 753 | @app.tool(
 754 |     description="Get Uyuşmazlık Mahkemesi decision text from URL in Markdown format",
 755 |     annotations={
 756 |         "readOnlyHint": True,
 757 |         "idempotentHint": True
 758 |     }
 759 | )
 760 | async def get_uyusmazlik_document_markdown_from_url(
 761 |     document_url: str = Field(..., description="Full URL to the Uyuşmazlık Mahkemesi decision document from search results")
 762 | ) -> Dict[str, Any]:
 763 |     """Get Uyuşmazlık Mahkemesi decision as Markdown."""
 764 |     logger.info(f"Tool 'get_uyusmazlik_document_markdown_from_url' called for URL: {str(document_url)}")
 765 |     if not document_url:
 766 |         raise ValueError("Document URL (document_url) is required for Uyuşmazlık document retrieval.")
 767 |     try:
 768 |         result = await uyusmazlik_client_instance.get_decision_document_as_markdown(str(document_url))
 769 |         return result.model_dump()
 770 |     except Exception as e:
 771 |         logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url'.")
 772 |         raise
 773 | 
 774 | # --- DEACTIVATED: MCP Tools for Anayasa Mahkemesi (Individual Tools) ---
 775 | # Use search_anayasa_unified and get_anayasa_document_unified instead
 776 | 
 777 | """
 778 | @app.tool(
 779 |     description="Search Constitutional Court norm control decisions with comprehensive filtering",
 780 |     annotations={
 781 |         "readOnlyHint": True,
 782 |         "openWorldHint": True,
 783 |         "idempotentHint": True
 784 |     }
 785 | )
 786 | # DEACTIVATED TOOL - Use search_anayasa_unified instead
 787 | # @app.tool(
 788 | #     description="DEACTIVATED - Use search_anayasa_unified instead",
 789 | #     annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True}
 790 | # )
 791 | # async def search_anayasa_norm_denetimi_decisions(...) -> AnayasaSearchResult:
 792 | #     raise ValueError("This tool is deactivated. Use search_anayasa_unified instead.")
 793 | 
 794 | # DEACTIVATED TOOL - Use get_anayasa_document_unified instead
 795 | # @app.tool(...)
 796 | # async def get_anayasa_norm_denetimi_document_markdown(...) -> AnayasaDocumentMarkdown:
 797 | #     raise ValueError("This tool is deactivated. Use get_anayasa_document_unified instead.")
 798 | 
 799 | # DEACTIVATED TOOL - Use search_anayasa_unified instead
 800 | # @app.tool(...)
 801 | # async def search_anayasa_bireysel_basvuru_report(...) -> AnayasaBireyselReportSearchResult:
 802 | #     raise ValueError("This tool is deactivated. Use search_anayasa_unified instead.")
 803 | 
 804 | # DEACTIVATED TOOL - Use get_anayasa_document_unified instead
 805 | # @app.tool(...)
 806 | # async def get_anayasa_bireysel_basvuru_document_markdown(...) -> AnayasaBireyselBasvuruDocumentMarkdown:
 807 | #     raise ValueError("This tool is deactivated. Use get_anayasa_document_unified instead.")
 808 | """
 809 | 
 810 | # --- Unified MCP Tools for Anayasa Mahkemesi ---
 811 | @app.tool(
 812 |     description="Unified search for Constitutional Court decisions: both norm control (normkararlarbilgibankasi) and individual applications (kararlarbilgibankasi) in one tool",
 813 |     annotations={
 814 |         "readOnlyHint": True,
 815 |         "openWorldHint": True,
 816 |         "idempotentHint": True
 817 |     }
 818 | )
 819 | async def search_anayasa_unified(
 820 |     decision_type: Literal["norm_denetimi", "bireysel_basvuru"] = Field(..., description="Decision type: norm_denetimi (norm control) or bireysel_basvuru (individual applications)"),
 821 |     keywords: List[str] = Field(default_factory=list, description="Keywords to search for (common parameter)"),
 822 |     page_to_fetch: int = Field(1, ge=1, le=100, description="Page number to fetch (1-100)"),
 823 |     # results_per_page: int = Field(10, ge=1, le=100, description="Results per page (1-100)"),
 824 |     
 825 |     # Norm Denetimi specific parameters (ignored for bireysel_basvuru)
 826 |     keywords_all: List[str] = Field(default_factory=list, description="All keywords must be present (norm_denetimi only)"),
 827 |     keywords_any: List[str] = Field(default_factory=list, description="Any of these keywords (norm_denetimi only)"),
 828 |     decision_type_norm: Literal["ALL", "1", "2", "3"] = Field("ALL", description="Decision type for norm denetimi"),
 829 |     application_date_start: str = Field("", description="Application start date (norm_denetimi only)"),
 830 |     application_date_end: str = Field("", description="Application end date (norm_denetimi only)"),
 831 |     
 832 |     # Bireysel Başvuru specific parameters (ignored for norm_denetimi)
 833 |     decision_start_date: str = Field("", description="Decision start date (bireysel_basvuru only)"),
 834 |     decision_end_date: str = Field("", description="Decision end date (bireysel_basvuru only)"),
 835 |     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)"),
 836 |     subject_category: str = Field("", description="Subject category (bireysel_basvuru only)")
 837 | ) -> str:
 838 |     logger.info(f"Tool 'search_anayasa_unified' called for decision_type: {decision_type}")
 839 |     
 840 |     results_per_page = 10  # Default value
 841 |     
 842 |     try:
 843 |         request = AnayasaUnifiedSearchRequest(
 844 |             decision_type=decision_type,
 845 |             keywords=keywords,
 846 |             page_to_fetch=page_to_fetch,
 847 |             results_per_page=results_per_page,
 848 |             keywords_all=keywords_all,
 849 |             keywords_any=keywords_any,
 850 |             decision_type_norm=decision_type_norm,
 851 |             application_date_start=application_date_start,
 852 |             application_date_end=application_date_end,
 853 |             decision_start_date=decision_start_date,
 854 |             decision_end_date=decision_end_date,
 855 |             norm_type=norm_type,
 856 |             subject_category=subject_category
 857 |         )
 858 |         
 859 |         result = await anayasa_unified_client_instance.search_unified(request)
 860 |         return json.dumps(result.model_dump(), ensure_ascii=False, indent=2)
 861 |         
 862 |     except Exception as e:
 863 |         logger.exception(f"Error in tool 'search_anayasa_unified'.")
 864 |         raise
 865 | 
 866 | @app.tool(
 867 |     description="Unified document retrieval for Constitutional Court decisions: auto-detects norm control vs individual applications based on URL",
 868 |     annotations={
 869 |         "readOnlyHint": True,
 870 |         "openWorldHint": False,
 871 |         "idempotentHint": True
 872 |     }
 873 | )
 874 | async def get_anayasa_document_unified(
 875 |     document_url: str = Field(..., description="Document URL from search results"),
 876 |     page_number: int = Field(1, ge=1, description="Page number for paginated content (1-indexed)")
 877 | ) -> str:
 878 |     logger.info(f"Tool 'get_anayasa_document_unified' called for URL: {document_url}, Page: {page_number}")
 879 |     
 880 |     try:
 881 |         result = await anayasa_unified_client_instance.get_document_unified(document_url, page_number)
 882 |         return json.dumps(result.model_dump(mode='json'), ensure_ascii=False, indent=2)
 883 |         
 884 |     except Exception as e:
 885 |         logger.exception(f"Error in tool 'get_anayasa_document_unified'.")
 886 |         raise
 887 | 
 888 | # --- MCP Tools for KIK v2 (Kamu İhale Kurulu - New API) ---
 889 | @app.tool(
 890 |     description="Search Public Procurement Authority (KİK) decisions using the new v2 API with JSON responses. Supports all three decision types: disputes (uyusmazlik), regulatory (duzenleyici), and court decisions (mahkeme)",
 891 |     annotations={
 892 |         "readOnlyHint": True,
 893 |         "openWorldHint": True,
 894 |         "idempotentHint": True
 895 |     }
 896 | )
 897 | async def search_kik_v2_decisions(
 898 |     decision_type: str = Field("uyusmazlik", description="Decision type: 'uyusmazlik' (disputes), 'duzenleyici' (regulatory), or 'mahkeme' (court decisions)"),
 899 |     karar_metni: str = Field("", description="Decision text search query"),
 900 |     karar_no: str = Field("", description="Decision number (e.g., '2025/UH.II-1801')"),
 901 |     basvuran: str = Field("", description="Applicant name"),
 902 |     idare_adi: str = Field("", description="Administration/procuring entity name"),
 903 |     baslangic_tarihi: str = Field("", description="Start date (YYYY-MM-DD format, e.g., '2025-01-01')"),
 904 |     bitis_tarihi: str = Field("", description="End date (YYYY-MM-DD format, e.g., '2025-12-31')")
 905 | ) -> dict:
 906 |     """Search Public Procurement Authority (KİK) decisions using the new v2 API.
 907 |     
 908 |     This tool supports all three KİK decision types:
 909 |     - uyusmazlik: Disputes and conflicts in public procurement
 910 |     - duzenleyici: Regulatory decisions and guidelines  
 911 |     - mahkeme: Court decisions and legal interpretations
 912 |     
 913 |     Each decision type uses its respective endpoint (GetKurulKararlari, GetKurulKararlariDk, GetKurulKararlariMk)
 914 |     and returns results with the decision_type field populated for identification.
 915 |     """
 916 |     
 917 |     logger.info(f"Tool 'search_kik_v2_decisions' called with decision_type='{decision_type}', karar_metni='{karar_metni}', karar_no='{karar_no}'")
 918 |     
 919 |     try:
 920 |         # Validate and convert decision type
 921 |         try:
 922 |             kik_decision_type = KikV2DecisionType(decision_type)
 923 |         except ValueError:
 924 |             return {
 925 |                 "decisions": [],
 926 |                 "total_records": 0,
 927 |                 "page": 1,
 928 |                 "error_code": "INVALID_DECISION_TYPE",
 929 |                 "error_message": f"Invalid decision type: {decision_type}. Valid options: uyusmazlik, duzenleyici, mahkeme"
 930 |             }
 931 |         
 932 |         api_response = await kik_v2_client_instance.search_decisions(
 933 |             decision_type=kik_decision_type,
 934 |             karar_metni=karar_metni,
 935 |             karar_no=karar_no,
 936 |             basvuran=basvuran,
 937 |             idare_adi=idare_adi,
 938 |             baslangic_tarihi=baslangic_tarihi,
 939 |             bitis_tarihi=bitis_tarihi
 940 |         )
 941 |         
 942 |         # Convert to dictionary for MCP tool response
 943 |         result = {
 944 |             "decisions": [decision.model_dump() for decision in api_response.decisions],
 945 |             "total_records": api_response.total_records,
 946 |             "page": api_response.page,
 947 |             "error_code": api_response.error_code,
 948 |             "error_message": api_response.error_message
 949 |         }
 950 |         
 951 |         logger.info(f"KİK v2 {decision_type} search completed. Found {len(api_response.decisions)} decisions")
 952 |         return result
 953 |         
 954 |     except Exception as e:
 955 |         logger.exception(f"Error in KİK v2 {decision_type} search tool 'search_kik_v2_decisions'.")
 956 |         return {
 957 |             "decisions": [],
 958 |             "total_records": 0,
 959 |             "page": 1,
 960 |             "error_code": "TOOL_ERROR",
 961 |             "error_message": str(e)
 962 |         }
 963 | 
 964 | @app.tool(
 965 |     description="Get Public Procurement Authority (KİK) decision document using the new v2 API (placeholder - full implementation pending)",
 966 |     annotations={
 967 |         "readOnlyHint": True,
 968 |         "idempotentHint": True
 969 |     }
 970 | )
 971 | async def get_kik_v2_document_markdown(
 972 |     document_id: str = Field(..., description="Document ID (gundemMaddesiId from search results)")
 973 | ) -> dict:
 974 |     """Get KİK decision document using the v2 API."""
 975 |     
 976 |     logger.info(f"Tool 'get_kik_v2_document_markdown' called for document ID: {document_id}")
 977 |     
 978 |     if not document_id or not document_id.strip():
 979 |         return {
 980 |             "document_id": document_id,
 981 |             "kararNo": "",
 982 |             "markdown_content": "",
 983 |             "source_url": "",
 984 |             "error_message": "Document ID is required and must be a non-empty string"
 985 |         }
 986 |     
 987 |     try:
 988 |         api_response = await kik_v2_client_instance.get_document_markdown(document_id)
 989 |         
 990 |         return {
 991 |             "document_id": api_response.document_id,
 992 |             "kararNo": api_response.kararNo,
 993 |             "markdown_content": api_response.markdown_content,
 994 |             "source_url": api_response.source_url,
 995 |             "error_message": api_response.error_message
 996 |         }
 997 |         
 998 |     except Exception as e:
 999 |         logger.exception(f"Error in KİK v2 document retrieval tool for document ID: {document_id}")
1000 |         return {
1001 |             "document_id": document_id,
1002 |             "kararNo": "",
1003 |             "markdown_content": "",
1004 |             "source_url": "",
1005 |             "error_message": f"Tool-level error during document retrieval: {str(e)}"
1006 |         }
1007 | @app.tool(
1008 |     description="Search Competition Authority (Rekabet Kurumu) decisions for competition law and antitrust",
1009 |     annotations={
1010 |         "readOnlyHint": True,
1011 |         "openWorldHint": True,
1012 |         "idempotentHint": True
1013 |     }
1014 | )
1015 | async def search_rekabet_kurumu_decisions(
1016 |     sayfaAdi: str = Field("", description="Search in decision title (Başlık)."),
1017 |     YayinlanmaTarihi: str = Field("", description="Publication date (Yayım Tarihi), e.g., DD.MM.YYYY."),
1018 |     PdfText: str = Field(
1019 |         "",
1020 |         description='Search in decision text. Use "\\"kesin cümle\\"" for precise matching.'
1021 |     ),
1022 |     KararTuru: Literal[ 
1023 |         "ALL", 
1024 |         "Birleşme ve Devralma",
1025 |         "Diğer",
1026 |         "Menfi Tespit ve Muafiyet",
1027 |         "Özelleştirme",
1028 |         "Rekabet İhlali"
1029 |     ] = Field("ALL", description="Parameter description"),
1030 |     KararSayisi: str = Field("", description="Decision number (Karar Sayısı)."),
1031 |     KararTarihi: str = Field("", description="Decision date (Karar Tarihi), e.g., DD.MM.YYYY."),
1032 |     page: int = Field(1, ge=1, description="Page number to fetch for the results list.")
1033 | ) -> Dict[str, Any]:
1034 |     """Search Competition Authority decisions."""
1035 |     
1036 |     karar_turu_guid_enum = KARAR_TURU_ADI_TO_GUID_ENUM_MAP.get(KararTuru)
1037 | 
1038 |     try:
1039 |         if karar_turu_guid_enum is None: 
1040 |             logger.warning(f"Invalid user-provided KararTuru: '{KararTuru}'. Defaulting to TUMU (all).")
1041 |             karar_turu_guid_enum = RekabetKararTuruGuidEnum.TUMU
1042 |     except Exception as e_map: 
1043 |         logger.error(f"Error mapping KararTuru '{KararTuru}': {e_map}. Defaulting to TUMU.")
1044 |         karar_turu_guid_enum = RekabetKararTuruGuidEnum.TUMU
1045 | 
1046 |     search_query = RekabetKurumuSearchRequest(
1047 |         sayfaAdi=sayfaAdi,
1048 |         YayinlanmaTarihi=YayinlanmaTarihi,
1049 |         PdfText=PdfText,
1050 |         KararTuruID=karar_turu_guid_enum, 
1051 |         KararSayisi=KararSayisi,
1052 |         KararTarihi=KararTarihi,
1053 |         page=page
1054 |     )
1055 |     logger.info(f"Tool 'search_rekabet_kurumu_decisions' called. Query: {search_query.model_dump_json(exclude_none=True, indent=2)}")
1056 |     try:
1057 |        
1058 |         result = await rekabet_client_instance.search_decisions(search_query)
1059 |         return result.model_dump()
1060 |     except Exception as e:
1061 |         logger.exception("Error in tool 'search_rekabet_kurumu_decisions'.")
1062 |         return RekabetSearchResult(decisions=[], retrieved_page_number=page, total_records_found=0, total_pages=0).model_dump()
1063 | 
1064 | @app.tool(
1065 |     description="Get Competition Authority decision text in paginated Markdown format",
1066 |     annotations={
1067 |         "readOnlyHint": True,
1068 |         "idempotentHint": True
1069 |     }
1070 | )
1071 | async def get_rekabet_kurumu_document(
1072 |     karar_id: str = Field(..., description="GUID (kararId) of the Rekabet Kurumu decision. This ID is obtained from search results."),
1073 |     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.")
1074 | ) -> Dict[str, Any]:
1075 |     """Get Competition Authority decision as paginated Markdown."""
1076 |     logger.info(f"Tool 'get_rekabet_kurumu_document' called. Karar ID: {karar_id}, Markdown Page: {page_number}")
1077 |     
1078 |     current_page_to_fetch = page_number if page_number >= 1 else 1
1079 |     
1080 |     try:
1081 |         result = await rekabet_client_instance.get_decision_document(karar_id, page_number=current_page_to_fetch)
1082 |         return result.model_dump()
1083 |     except Exception as e:
1084 |         logger.exception(f"Error in tool 'get_rekabet_kurumu_document'. Karar ID: {karar_id}")
1085 |         raise 
1086 | 
1087 | # --- MCP Tools for Bedesten (Unified Search Across All Courts) ---
1088 | @app.tool(
1089 |     description="Search multiple Turkish courts (Yargıtay, Danıştay, Local Courts, Appeals Courts, KYB)",
1090 |     annotations={
1091 |         "readOnlyHint": True,
1092 |         "openWorldHint": True,
1093 |         "idempotentHint": True
1094 |     }
1095 | )
1096 | async def search_bedesten_unified(
1097 |     ctx: Context,
1098 |     phrase: str = Field(..., description="""Search query in Turkish. SUPPORTED OPERATORS:
1099 | • Simple: "mülkiyet hakkı" (finds both words)
1100 | • Exact phrase: "\"mülkiyet hakkı\"" (finds exact phrase)  
1101 | • Required term: "+mülkiyet hakkı" (must contain mülkiyet)
1102 | • Exclude term: "mülkiyet -kira" (contains mülkiyet but not kira)
1103 | • Boolean AND: "mülkiyet AND hak" (both terms required)
1104 | • Boolean OR: "mülkiyet OR tapu" (either term acceptable)
1105 | • Boolean NOT: "mülkiyet NOT satış" (contains mülkiyet but not satış)
1106 | NOTE: Wildcards (*,?), regex patterns (/regex/), fuzzy search (~), and proximity search are NOT supported.
1107 | For best results, use exact phrases with quotes for legal terms."""),
1108 |     court_types: List[BedestenCourtTypeEnum] = Field(
1109 |         default=["YARGITAYKARARI", "DANISTAYKARAR"], 
1110 |         description="Court types: YARGITAYKARARI, DANISTAYKARAR, YERELHUKUK, ISTINAFHUKUK, KYB"
1111 |     ),
1112 |     # pageSize: int = Field(10, ge=1, le=10, description="Results per page (1-10)"),
1113 |     pageNumber: int = Field(1, ge=1, description="Page number"),
1114 |     birimAdi: BirimAdiEnum = Field("ALL", description="""
1115 |         Chamber filter (optional). Abbreviated values with Turkish names:
1116 |         • 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)
1117 |         • 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)
1118 |         """),
1119 |     kararTarihiStart: str = Field("", description="Start date (ISO 8601 format)"),
1120 |     kararTarihiEnd: str = Field("", description="End date (ISO 8601 format)")
1121 | ) -> dict:
1122 |     """Search Turkish legal databases via unified Bedesten API."""
1123 |     
1124 |     # Get Bearer token information for access control and logging
1125 |     try:
1126 |         access_token: AccessToken = get_access_token()
1127 |         user_id = access_token.client_id
1128 |         user_scopes = access_token.scopes
1129 |         
1130 |         # Check for required scopes - DISABLED: Already handled by Bearer auth provider
1131 |         # if "yargi.read" not in user_scopes and "yargi.search" not in user_scopes:
1132 |         #     raise ToolError(f"Insufficient permissions: 'yargi.read' or 'yargi.search' scope required. Current scopes: {user_scopes}")
1133 |         
1134 |         logger.info(f"Tool 'search_bedesten_unified' called by user '{user_id}' with scopes {user_scopes}")
1135 |         
1136 |     except Exception as e:
1137 |         # Development mode fallback - allow access without strict token validation
1138 |         logger.warning(f"Bearer token validation failed, using development mode: {str(e)}")
1139 |         user_id = "dev-user"
1140 |         user_scopes = ["yargi.read", "yargi.search"]
1141 |     
1142 |     pageSize = 10  # Default value
1143 |     
1144 |     # Convert date formats if provided
1145 |     # Accept formats: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS.000Z
1146 |     if kararTarihiStart and not kararTarihiStart.endswith('Z'):
1147 |         # Convert simple date format to ISO 8601 with timezone
1148 |         if 'T' not in kararTarihiStart:
1149 |             kararTarihiStart = f"{kararTarihiStart}T00:00:00.000Z"
1150 |     
1151 |     if kararTarihiEnd and not kararTarihiEnd.endswith('Z'):
1152 |         # Convert simple date format to ISO 8601 with timezone
1153 |         if 'T' not in kararTarihiEnd:
1154 |             kararTarihiEnd = f"{kararTarihiEnd}T23:59:59.999Z"
1155 |     
1156 |     search_data = BedestenSearchData(
1157 |         pageSize=pageSize,
1158 |         pageNumber=pageNumber,
1159 |         itemTypeList=court_types,
1160 |         phrase=phrase,
1161 |         birimAdi=birimAdi,
1162 |         kararTarihiStart=kararTarihiStart,
1163 |         kararTarihiEnd=kararTarihiEnd
1164 |     )
1165 |     
1166 |     search_request = BedestenSearchRequest(data=search_data)
1167 |     
1168 |     logger.info(f"User '{user_id}' searching bedesten: phrase='{phrase}', court_types={court_types}, birimAdi='{birimAdi}', page={pageNumber}")
1169 |     
1170 |     try:
1171 |         response = await bedesten_client_instance.search_documents(search_request)
1172 |         
1173 |         if response.data is None:
1174 |             return {
1175 |                 "decisions": [],
1176 |                 "total_records": 0,
1177 |                 "requested_page": pageNumber,
1178 |                 "page_size": pageSize,
1179 |                 "searched_courts": court_types,
1180 |                 "error": "No data returned from Bedesten API"
1181 |             }
1182 |         
1183 |         # Add null safety checks for response.data fields
1184 |         emsal_karar_list = response.data.emsalKararList if hasattr(response.data, 'emsalKararList') and response.data.emsalKararList is not None else []
1185 |         total_records = response.data.total if hasattr(response.data, 'total') and response.data.total is not None else 0
1186 |         
1187 |         return {
1188 |             "decisions": [d.model_dump() for d in emsal_karar_list],
1189 |             "total_records": total_records,
1190 |             "requested_page": pageNumber,
1191 |             "page_size": pageSize,
1192 |             "searched_courts": court_types
1193 |         }
1194 |     except Exception as e:
1195 |         logger.exception("Error in tool 'search_bedesten_unified'")
1196 |         raise
1197 | 
1198 | @app.tool(
1199 |     description="Get legal decision document from Bedesten API in Markdown format",
1200 |     annotations={
1201 |         "readOnlyHint": True,
1202 |         "idempotentHint": True
1203 |     }
1204 | )
1205 | async def get_bedesten_document_markdown(
1206 |     documentId: str = Field(..., description="Document ID from Bedesten search results")
1207 | ) -> BedestenDocumentMarkdown:
1208 |     """Get legal decision document as Markdown from Bedesten API."""
1209 |     logger.info(f"Tool 'get_bedesten_document_markdown' called for ID: {documentId}")
1210 |     
1211 |     if not documentId or not documentId.strip():
1212 |         raise ValueError("Document ID must be a non-empty string.")
1213 |     
1214 |     try:
1215 |         return await bedesten_client_instance.get_document_as_markdown(documentId)
1216 |     except Exception as e:
1217 |         logger.exception("Error in tool 'get_kyb_bedesten_document_markdown'")
1218 |         raise
1219 | 
1220 | # --- MCP Tools for Sayıştay (Turkish Court of Accounts) ---
1221 | 
1222 | # DEACTIVATED TOOL - Use search_sayistay_unified instead
1223 | # @app.tool(
1224 | #     description="Search Sayıştay Genel Kurul decisions for audit and accountability regulations",
1225 | #     annotations={
1226 | #         "readOnlyHint": True,
1227 | #         "openWorldHint": True,
1228 | #         "idempotentHint": True
1229 | #     }
1230 | # )
1231 | # async def search_sayistay_genel_kurul(
1232 | #     karar_no: str = Field("", description="Decision number to search for (e.g., '5415')"),
1233 | #     karar_ek: str = Field("", description="Decision appendix number (max 99, e.g., '1')"),
1234 | #     karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY)"),
1235 | #     karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY)"),
1236 | #     karar_tamami: str = Field("", description="Full text search"),
1237 | #     start: int = Field(0, description="Starting record for pagination (0-based)"),
1238 | #     length: int = Field(10, description="Number of records per page (1-100)")
1239 | # ) -> GenelKurulSearchResponse:
1240 | #     """Search Sayıştay General Assembly decisions."""
1241 | #     raise ValueError("This tool is deactivated. Use search_sayistay_unified instead.")
1242 | 
1243 | # DEACTIVATED TOOL - Use search_sayistay_unified instead
1244 | # @app.tool(
1245 | #     description="Search Sayıştay Temyiz Kurulu decisions with chamber filtering and comprehensive criteria",
1246 | #     annotations={
1247 | #         "readOnlyHint": True,
1248 | #         "openWorldHint": True,
1249 | #         "idempotentHint": True
1250 | #     }
1251 | # )
1252 | # async def search_sayistay_temyiz_kurulu(
1253 | #     ilam_dairesi: DaireEnum = Field("ALL", description="Audit chamber selection"),
1254 | #     yili: str = Field("", description="Year (YYYY)"),
1255 | #     karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY)"),
1256 | #     karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY)"),
1257 | #     kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Public admin type"),
1258 | #     ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)"),
1259 | #     dosya_no: str = Field("", description="File number for the case"),
1260 | #     temyiz_tutanak_no: str = Field("", description="Appeals board meeting minutes number"),
1261 | #     temyiz_karar: str = Field("", description="Appeals decision text"),
1262 | #     web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Decision subject"),
1263 | #     start: int = Field(0, description="Starting record for pagination (0-based)"),
1264 | #     length: int = Field(10, description="Number of records per page (1-100)")
1265 | # ) -> TemyizKuruluSearchResponse:
1266 | #     """Search Sayıştay Appeals Board decisions."""
1267 | #     raise ValueError("This tool is deactivated. Use search_sayistay_unified instead.")
1268 | 
1269 | # DEACTIVATED TOOL - Use search_sayistay_unified instead
1270 | # @app.tool(
1271 | #     description="Search Sayıştay Daire decisions with chamber filtering and subject categorization",
1272 | #     annotations={
1273 | #         "readOnlyHint": True,
1274 | #         "openWorldHint": True,
1275 | #         "idempotentHint": True
1276 | #     }
1277 | # )
1278 | # async def search_sayistay_daire(
1279 | #     yargilama_dairesi: DaireEnum = Field("ALL", description="Chamber selection"),
1280 | #     karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY)"),
1281 | #     karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY)"),
1282 | #     ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)"),
1283 | #     kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Public admin type"),
1284 | #     hesap_yili: str = Field("", description="Fiscal year"),
1285 | #     web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Decision subject"),
1286 | #     web_karar_metni: str = Field("", description="Decision text search"),
1287 | #     start: int = Field(0, description="Starting record for pagination (0-based)"),
1288 | #     length: int = Field(10, description="Number of records per page (1-100)")
1289 | # ) -> DaireSearchResponse:
1290 | #     """Search Sayıştay Chamber decisions."""
1291 | #     raise ValueError("This tool is deactivated. Use search_sayistay_unified instead.")
1292 | 
1293 | # DEACTIVATED TOOL - Use get_sayistay_document_unified instead
1294 | # @app.tool(
1295 | #     description="Get Sayıştay Genel Kurul decision document in Markdown format",
1296 | #     annotations={
1297 | #         "readOnlyHint": True,
1298 | #         "openWorldHint": False,
1299 | #         "idempotentHint": True
1300 | #     }
1301 | # )
1302 | # async def get_sayistay_genel_kurul_document_markdown(
1303 | #     decision_id: str = Field(..., description="Decision ID from search_sayistay_genel_kurul results")
1304 | # ) -> SayistayDocumentMarkdown:
1305 | #     """Get Sayıştay General Assembly decision as Markdown."""
1306 | #     raise ValueError("This tool is deactivated. Use get_sayistay_document_unified instead.")
1307 | 
1308 | # DEACTIVATED TOOL - Use get_sayistay_document_unified instead
1309 | # @app.tool(
1310 | #     description="Get Sayıştay Temyiz Kurulu decision document in Markdown format",
1311 | #     annotations={
1312 | #         "readOnlyHint": True,
1313 | #         "openWorldHint": False,
1314 | #         "idempotentHint": True
1315 | #     }
1316 | # )
1317 | # async def get_sayistay_temyiz_kurulu_document_markdown(
1318 | #     decision_id: str = Field(..., description="Decision ID from search_sayistay_temyiz_kurulu results")
1319 | # ) -> SayistayDocumentMarkdown:
1320 | #     """Get Sayıştay Appeals Board decision as Markdown."""
1321 | #     raise ValueError("This tool is deactivated. Use get_sayistay_document_unified instead.")
1322 | 
1323 | # DEACTIVATED TOOL - Use get_sayistay_document_unified instead
1324 | # @app.tool(
1325 | #     description="Get Sayıştay Daire decision document in Markdown format",
1326 | #     annotations={
1327 | #         "readOnlyHint": True,
1328 | #         "openWorldHint": False,
1329 | #         "idempotentHint": True
1330 | #     }
1331 | # )
1332 | # async def get_sayistay_daire_document_markdown(
1333 | #     decision_id: str = Field(..., description="Decision ID from search_sayistay_daire results")
1334 | # ) -> SayistayDocumentMarkdown:
1335 | #     """Get Sayıştay Chamber decision as Markdown."""
1336 | #     raise ValueError("This tool is deactivated. Use get_sayistay_document_unified instead.")
1337 | 
1338 | # --- UNIFIED MCP Tools for Sayıştay (Turkish Court of Accounts) ---
1339 | 
1340 | @app.tool(
1341 |     description="Search Sayıştay decisions unified across all three decision types (Genel Kurul, Temyiz Kurulu, Daire) with comprehensive filtering",
1342 |     annotations={
1343 |         "readOnlyHint": True,
1344 |         "openWorldHint": True,
1345 |         "idempotentHint": True
1346 |     }
1347 | )
1348 | async def search_sayistay_unified(
1349 |     decision_type: Literal["genel_kurul", "temyiz_kurulu", "daire"] = Field(..., description="Decision type: genel_kurul, temyiz_kurulu, or daire"),
1350 |     
1351 |     # Common pagination parameters
1352 |     start: int = Field(0, ge=0, description="Starting record for pagination (0-based)"),
1353 |     length: int = Field(10, ge=1, le=100, description="Number of records per page (1-100)"),
1354 |     
1355 |     # Common search parameters
1356 |     karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY format)"),
1357 |     karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY format)"),
1358 |     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"),
1359 |     ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)"),
1360 |     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"),
1361 |     
1362 |     # Genel Kurul specific parameters (ignored for other types)
1363 |     karar_no: str = Field("", description="Decision number (genel_kurul only)"),
1364 |     karar_ek: str = Field("", description="Decision appendix number (genel_kurul only)"),
1365 |     karar_tamami: str = Field("", description="Full text search (genel_kurul only)"),
1366 |     
1367 |     # Temyiz Kurulu specific parameters (ignored for other types)
1368 |     ilam_dairesi: Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8"] = Field("ALL", description="Audit chamber selection (temyiz_kurulu only)"),
1369 |     yili: str = Field("", description="Year (YYYY format, temyiz_kurulu only)"),
1370 |     dosya_no: str = Field("", description="File number (temyiz_kurulu only)"),
1371 |     temyiz_tutanak_no: str = Field("", description="Appeals board meeting minutes number (temyiz_kurulu only)"),
1372 |     temyiz_karar: str = Field("", description="Appeals decision text search (temyiz_kurulu only)"),
1373 |     
1374 |     # Daire specific parameters (ignored for other types)
1375 |     yargilama_dairesi: Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8"] = Field("ALL", description="Chamber selection (daire only)"),
1376 |     hesap_yili: str = Field("", description="Account year (daire only)"),
1377 |     web_karar_metni: str = Field("", description="Decision text search (daire only)")
1378 | ) -> Dict[str, Any]:
1379 |     """Search Sayıştay decisions across all three decision types with unified interface."""
1380 |     logger.info(f"Tool 'search_sayistay_unified' called with decision_type={decision_type}")
1381 | 
1382 |     try:
1383 |         search_request = SayistayUnifiedSearchRequest(
1384 |             decision_type=decision_type,
1385 |             start=start,
1386 |             length=length,
1387 |             karar_tarih_baslangic=karar_tarih_baslangic,
1388 |             karar_tarih_bitis=karar_tarih_bitis,
1389 |             kamu_idaresi_turu=kamu_idaresi_turu,
1390 |             ilam_no=ilam_no,
1391 |             web_karar_konusu=web_karar_konusu,
1392 |             karar_no=karar_no,
1393 |             karar_ek=karar_ek,
1394 |             karar_tamami=karar_tamami,
1395 |             ilam_dairesi=ilam_dairesi,
1396 |             yili=yili,
1397 |             dosya_no=dosya_no,
1398 |             temyiz_tutanak_no=temyiz_tutanak_no,
1399 |             temyiz_karar=temyiz_karar,
1400 |             yargilama_dairesi=yargilama_dairesi,
1401 |             hesap_yili=hesap_yili,
1402 |             web_karar_metni=web_karar_metni
1403 |         )
1404 |         result = await sayistay_unified_client_instance.search_unified(search_request)
1405 |         return result.model_dump()
1406 |     except Exception as e:
1407 |         logger.exception("Error in tool 'search_sayistay_unified'")
1408 |         raise
1409 | 
1410 | @app.tool(
1411 |     description="Get Sayıştay decision document in Markdown format for any decision type",
1412 |     annotations={
1413 |         "readOnlyHint": True,
1414 |         "openWorldHint": False,
1415 |         "idempotentHint": True
1416 |     }
1417 | )
1418 | async def get_sayistay_document_unified(
1419 |     decision_id: str = Field(..., description="Decision ID from search_sayistay_unified results"),
1420 |     decision_type: Literal["genel_kurul", "temyiz_kurulu", "daire"] = Field(..., description="Decision type: genel_kurul, temyiz_kurulu, or daire")
1421 | ) -> Dict[str, Any]:
1422 |     """Get Sayıştay decision document as Markdown for any decision type."""
1423 |     logger.info(f"Tool 'get_sayistay_document_unified' called for ID: {decision_id}, type: {decision_type}")
1424 | 
1425 |     if not decision_id or not decision_id.strip():
1426 |         raise ValueError("Decision ID must be a non-empty string.")
1427 | 
1428 |     try:
1429 |         result = await sayistay_unified_client_instance.get_document_unified(decision_id, decision_type)
1430 |         return result.model_dump()
1431 |     except Exception as e:
1432 |         logger.exception("Error in tool 'get_sayistay_document_unified'")
1433 |         raise
1434 | 
1435 | # --- Application Shutdown Handling ---
1436 | def perform_cleanup():
1437 |     logger.info("MCP Server performing cleanup...")
1438 |     try:
1439 |         loop = asyncio.get_event_loop_policy().get_event_loop()
1440 |         if loop.is_closed(): 
1441 |             loop = asyncio.new_event_loop()
1442 |             asyncio.set_event_loop(loop)
1443 |     except RuntimeError: 
1444 |         loop = asyncio.new_event_loop()
1445 |         asyncio.set_event_loop(loop)
1446 |     clients_to_close = [
1447 |         globals().get('yargitay_client_instance'),
1448 |         globals().get('danistay_client_instance'),
1449 |         globals().get('emsal_client_instance'),
1450 |         globals().get('uyusmazlik_client_instance'),
1451 |         globals().get('anayasa_norm_client_instance'),
1452 |         globals().get('anayasa_bireysel_client_instance'),
1453 |         globals().get('anayasa_unified_client_instance'),
1454 |         globals().get('kik_v2_client_instance'),
1455 |         globals().get('rekabet_client_instance'),
1456 |         globals().get('bedesten_client_instance'),
1457 |         globals().get('sayistay_client_instance'),
1458 |         globals().get('sayistay_unified_client_instance'),
1459 |         globals().get('kvkk_client_instance'),
1460 |         globals().get('bddk_client_instance')
1461 |     ]
1462 |     async def close_all_clients_async():
1463 |         tasks = []
1464 |         for client_instance in clients_to_close:
1465 |             if client_instance and hasattr(client_instance, 'close_client_session') and callable(client_instance.close_client_session):
1466 |                 logger.info(f"Scheduling close for client session: {client_instance.__class__.__name__}")
1467 |                 tasks.append(client_instance.close_client_session())
1468 |         if tasks:
1469 |             results = await asyncio.gather(*tasks, return_exceptions=True)
1470 |             for i, result in enumerate(results):
1471 |                 if isinstance(result, Exception):
1472 |                     client_name = "Unknown Client"
1473 |                     if i < len(clients_to_close) and clients_to_close[i] is not None:
1474 |                         client_name = clients_to_close[i].__class__.__name__
1475 |                     logger.error(f"Error closing client {client_name}: {result}")
1476 |     try:
1477 |         if loop.is_running(): 
1478 |             asyncio.ensure_future(close_all_clients_async(), loop=loop)
1479 |             logger.info("Client cleanup tasks scheduled on running event loop.")
1480 |         else:
1481 |             loop.run_until_complete(close_all_clients_async())
1482 |             logger.info("Client cleanup tasks completed via run_until_complete.")
1483 |     except Exception as e: 
1484 |         logger.error(f"Error during atexit cleanup execution: {e}", exc_info=True)
1485 |     logger.info("MCP Server atexit cleanup process finished.")
1486 | 
1487 | atexit.register(perform_cleanup)
1488 | 
1489 | # --- Health Check Tools ---
1490 | @app.tool(
1491 |     description="Check if Turkish government legal database servers are operational",
1492 |     annotations={
1493 |         "readOnlyHint": True,
1494 |         "idempotentHint": True
1495 |     }
1496 | )
1497 | async def check_government_servers_health() -> Dict[str, Any]:
1498 |     """Check health status of Turkish government legal database servers."""
1499 |     logger.info("Health check tool called for government servers")
1500 |     
1501 |     health_results = {}
1502 |     
1503 |     # Check Yargıtay server
1504 |     try:
1505 |         yargitay_payload = {
1506 |             "data": {
1507 |                 "aranan": "karar",
1508 |                 "arananKelime": "karar", 
1509 |                 "pageSize": 10,
1510 |                 "pageNumber": 1
1511 |             }
1512 |         }
1513 |         
1514 |         async with httpx.AsyncClient(
1515 |             headers={
1516 |                 "Accept": "*/*",
1517 |                 "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
1518 |                 "Connection": "keep-alive",
1519 |                 "Content-Type": "application/json; charset=UTF-8",
1520 |                 "Origin": "https://karararama.yargitay.gov.tr",
1521 |                 "Referer": "https://karararama.yargitay.gov.tr/",
1522 |                 "Sec-Fetch-Dest": "empty",
1523 |                 "Sec-Fetch-Mode": "cors", 
1524 |                 "Sec-Fetch-Site": "same-origin",
1525 |                 "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",
1526 |                 "X-Requested-With": "XMLHttpRequest"
1527 |             },
1528 |             timeout=30.0,
1529 |             verify=False
1530 |         ) as client:
1531 |             response = await client.post(
1532 |                 "https://karararama.yargitay.gov.tr/aramalist",
1533 |                 json=yargitay_payload
1534 |             )
1535 |         
1536 |         if response.status_code == 200:
1537 |             response_data = response.json()
1538 |             records_total = response_data.get("data", {}).get("recordsTotal", 0)
1539 |             
1540 |             if records_total > 0:
1541 |                 health_results["yargitay"] = {
1542 |                     "status": "healthy",
1543 |                     "response_time_ms": response.elapsed.total_seconds() * 1000
1544 |                 }
1545 |             else:
1546 |                 health_results["yargitay"] = {
1547 |                     "status": "unhealthy", 
1548 |                     "reason": "recordsTotal is 0 or missing",
1549 |                     "response_time_ms": response.elapsed.total_seconds() * 1000
1550 |                 }
1551 |         else:
1552 |             health_results["yargitay"] = {
1553 |                 "status": "unhealthy", 
1554 |                 "reason": f"HTTP {response.status_code}",
1555 |                 "response_time_ms": response.elapsed.total_seconds() * 1000
1556 |             }
1557 |         
1558 |     except Exception as e:
1559 |         health_results["yargitay"] = {
1560 |             "status": "unhealthy",
1561 |             "reason": f"Connection error: {str(e)}"
1562 |         }
1563 |     
1564 |     # Check Bedesten API server
1565 |     try:
1566 |         bedesten_payload = {
1567 |             "data": {
1568 |                 "pageSize": 5,
1569 |                 "pageNumber": 1,
1570 |                 "itemTypeList": ["YARGITAYKARARI"], 
1571 |                 "phrase": "karar",
1572 |                 "sortFields": ["KARAR_TARIHI"],
1573 |                 "sortDirection": "desc"
1574 |             },
1575 |             "applicationName": "UyapMevzuat",
1576 |             "paging": True
1577 |         }
1578 |         
1579 |         client = get_or_create_health_check_client()
1580 |         headers = {
1581 |             "Content-Type": "application/json",
1582 |             "Accept": "application/json",
1583 |             "User-Agent": "Mozilla/5.0 Health Check"
1584 |         }
1585 |         
1586 |         response = await client.post(
1587 |             "https://bedesten.adalet.gov.tr/emsal-karar/searchDocuments",
1588 |             json=bedesten_payload,
1589 |             headers=headers
1590 |         )
1591 |         
1592 |         if response.status_code == 200:
1593 |             response_data = response.json()
1594 |             logger.debug(f"Bedesten API response: {response_data}")
1595 |             if response_data and isinstance(response_data, dict):
1596 |                 data_section = response_data.get("data")
1597 |                 if data_section and isinstance(data_section, dict):
1598 |                     total_found = data_section.get("total", 0)
1599 |                 else:
1600 |                     total_found = 0
1601 |             else:
1602 |                 total_found = 0
1603 |             
1604 |             if total_found > 0:
1605 |                 health_results["bedesten"] = {
1606 |                     "status": "healthy", 
1607 |                     "response_time_ms": response.elapsed.total_seconds() * 1000
1608 |                 }
1609 |             else:
1610 |                 health_results["bedesten"] = {
1611 |                     "status": "unhealthy",
1612 |                     "reason": "total is 0 or missing in data field",
1613 |                     "response_time_ms": response.elapsed.total_seconds() * 1000
1614 |                 }
1615 |         else:
1616 |             health_results["bedesten"] = {
1617 |                 "status": "unhealthy",
1618 |                 "reason": f"HTTP {response.status_code}",
1619 |                 "response_time_ms": response.elapsed.total_seconds() * 1000
1620 |             }
1621 |         
1622 |     except Exception as e:
1623 |         health_results["bedesten"] = {
1624 |             "status": "unhealthy", 
1625 |             "reason": f"Connection error: {str(e)}"
1626 |         }
1627 |     
1628 |     # Overall health assessment
1629 |     healthy_servers = sum(1 for server in health_results.values() if server["status"] == "healthy")
1630 |     total_servers = len(health_results)
1631 |     
1632 |     overall_status = "healthy" if healthy_servers == total_servers else "degraded" if healthy_servers > 0 else "unhealthy"
1633 |     
1634 |     return {
1635 |         "overall_status": overall_status,
1636 |         "healthy_servers": healthy_servers,
1637 |         "total_servers": total_servers,
1638 |         "servers": health_results,
1639 |         "check_timestamp": f"{__import__('datetime').datetime.now().isoformat()}"
1640 |     }
1641 | 
1642 | # --- MCP Tools for KVKK ---
1643 | @app.tool(
1644 |     description="Search KVKK data protection authority decisions",
1645 |     annotations={
1646 |         "readOnlyHint": True,
1647 |         "openWorldHint": True,
1648 |         "idempotentHint": True
1649 |     }
1650 | )
1651 | async def search_kvkk_decisions(
1652 |     keywords: str = Field(..., description="Turkish keywords. Supports +required -excluded \"exact phrase\" operators"),
1653 |     page: int = Field(1, ge=1, le=50, description="Page number for results (1-50)."),
1654 |     # pageSize: int = Field(10, ge=1, le=20, description="Number of results per page (1-20).")
1655 | ) -> Dict[str, Any]:
1656 |     """Search function for legal decisions."""
1657 |     logger.info(f"KVKK search tool called with keywords: {keywords}")
1658 | 
1659 |     pageSize = 10  # Default value
1660 | 
1661 |     search_request = KvkkSearchRequest(
1662 |         keywords=keywords,
1663 |         page=page,
1664 |         pageSize=pageSize
1665 |     )
1666 | 
1667 |     try:
1668 |         result = await kvkk_client_instance.search_decisions(search_request)
1669 |         logger.info(f"KVKK search completed. Found {len(result.decisions)} decisions on page {page}")
1670 |         return result.model_dump()
1671 |     except Exception as e:
1672 |         logger.exception(f"Error in KVKK search: {e}")
1673 |         # Return empty result on error
1674 |         return KvkkSearchResult(
1675 |             decisions=[],
1676 |             total_results=0,
1677 |             page=page,
1678 |             pageSize=pageSize,
1679 |             query=keywords
1680 |         ).model_dump()
1681 | 
1682 | @app.tool(
1683 |     description="Get KVKK decision document in Markdown format with metadata extraction",
1684 |     annotations={
1685 |         "readOnlyHint": True,
1686 |         "openWorldHint": False,
1687 |         "idempotentHint": True
1688 |     }
1689 | )
1690 | async def get_kvkk_document_markdown(
1691 |     decision_url: str = Field(..., description="KVKK decision URL from search results"),
1692 |     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).")
1693 | ) -> Dict[str, Any]:
1694 |     """Get KVKK decision as paginated Markdown."""
1695 |     logger.info(f"KVKK document retrieval tool called for URL: {decision_url}")
1696 | 
1697 |     if not decision_url or not decision_url.strip():
1698 |         return KvkkDocumentMarkdown(
1699 |             source_url=HttpUrl("https://www.kvkk.gov.tr"),
1700 |             title=None,
1701 |             decision_date=None,
1702 |             decision_number=None,
1703 |             subject_summary=None,
1704 |             markdown_chunk=None,
1705 |             current_page=page_number or 1,
1706 |             total_pages=0,
1707 |             is_paginated=False,
1708 |             error_message="Decision URL is required and cannot be empty."
1709 |         ).model_dump()
1710 |     
1711 |     try:
1712 |         # Validate URL format
1713 |         if not decision_url.startswith("https://www.kvkk.gov.tr/"):
1714 |             return KvkkDocumentMarkdown(
1715 |                 source_url=HttpUrl(decision_url),
1716 |                 title=None,
1717 |                 decision_date=None,
1718 |                 decision_number=None,
1719 |                 subject_summary=None,
1720 |                 markdown_chunk=None,
1721 |                 current_page=page_number or 1,
1722 |                 total_pages=0,
1723 |                 is_paginated=False,
1724 |                 error_message="Invalid KVKK decision URL format. URL must start with https://www.kvkk.gov.tr/"
1725 |             ).model_dump()
1726 | 
1727 |         result = await kvkk_client_instance.get_decision_document(decision_url, page_number or 1)
1728 |         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}")
1729 |         return result.model_dump()
1730 |         
1731 |     except Exception as e:
1732 |         logger.exception(f"Error retrieving KVKK document: {e}")
1733 |         return KvkkDocumentMarkdown(
1734 |             source_url=HttpUrl(decision_url),
1735 |             title=None,
1736 |             decision_date=None,
1737 |             decision_number=None,
1738 |             subject_summary=None,
1739 |             markdown_chunk=None,
1740 |             current_page=page_number or 1,
1741 |             total_pages=0,
1742 |             is_paginated=False,
1743 |             error_message=f"Error retrieving KVKK document: {str(e)}"
1744 |         ).model_dump()
1745 | 
1746 | # --- MCP Tools for BDDK (Banking Regulation Authority) ---
1747 | @app.tool(
1748 |     description="Search BDDK banking regulation decisions",
1749 |     annotations={
1750 |         "readOnlyHint": True,
1751 |         "openWorldHint": True,
1752 |         "idempotentHint": True
1753 |     }
1754 | )
1755 | async def search_bddk_decisions(
1756 |     keywords: str = Field(..., description="Search keywords in Turkish"),
1757 |     page: int = Field(1, ge=1, description="Page number")
1758 |     # pageSize: int = Field(10, ge=1, le=50, description="Results per page")
1759 | ) -> dict:
1760 |     """Search BDDK banking regulation and supervision decisions."""
1761 |     logger.info(f"BDDK search tool called with keywords: {keywords}, page: {page}")
1762 |     
1763 |     pageSize = 10  # Default value
1764 |     
1765 |     try:
1766 |         search_request = BddkSearchRequest(
1767 |             keywords=keywords,
1768 |             page=page,
1769 |             pageSize=pageSize
1770 |         )
1771 |         
1772 |         result = await bddk_client_instance.search_decisions(search_request)
1773 |         logger.info(f"BDDK search completed. Found {len(result.decisions)} decisions on page {page}")
1774 |         
1775 |         return {
1776 |             "decisions": [
1777 |                 {
1778 |                     "title": dec.title,
1779 |                     "document_id": dec.document_id,
1780 |                     "content": dec.content
1781 |                 }
1782 |                 for dec in result.decisions
1783 |             ],
1784 |             "total_results": result.total_results,
1785 |             "page": result.page,
1786 |             "pageSize": result.pageSize
1787 |         }
1788 |     
1789 |     except Exception as e:
1790 |         logger.exception(f"Error searching BDDK decisions: {e}")
1791 |         return {
1792 |             "decisions": [],
1793 |             "total_results": 0,
1794 |             "page": page,
1795 |             "pageSize": pageSize,
1796 |             "error": str(e)
1797 |         }
1798 | 
1799 | @app.tool(
1800 |     description="Get BDDK decision document as Markdown",
1801 |     annotations={
1802 |         "readOnlyHint": True,
1803 |         "openWorldHint": False,
1804 |         "idempotentHint": True
1805 |     }
1806 | )
1807 | async def get_bddk_document_markdown(
1808 |     document_id: str = Field(..., description="BDDK document ID (e.g., '310')"),
1809 |     page_number: int = Field(1, ge=1, description="Page number")
1810 | ) -> dict:
1811 |     """Retrieve BDDK decision document in Markdown format."""
1812 |     logger.info(f"BDDK document retrieval tool called for ID: {document_id}, page: {page_number}")
1813 |     
1814 |     if not document_id or not document_id.strip():
1815 |         return {
1816 |             "document_id": document_id,
1817 |             "markdown_content": "",
1818 |             "page_number": page_number,
1819 |             "total_pages": 0,
1820 |             "error": "Document ID is required"
1821 |         }
1822 |     
1823 |     try:
1824 |         result = await bddk_client_instance.get_document_markdown(document_id, page_number)
1825 |         logger.info(f"BDDK document retrieved successfully. Page {result.page_number}/{result.total_pages}")
1826 |         
1827 |         return {
1828 |             "document_id": result.document_id,
1829 |             "markdown_content": result.markdown_content,
1830 |             "page_number": result.page_number,
1831 |             "total_pages": result.total_pages
1832 |         }
1833 |         
1834 |     except Exception as e:
1835 |         logger.exception(f"Error retrieving BDDK document: {e}")
1836 |         return {
1837 |             "document_id": document_id,
1838 |             "markdown_content": "",
1839 |             "page_number": page_number,
1840 |             "total_pages": 0,
1841 |             "error": str(e)
1842 |         }
1843 | 
1844 | # --- ChatGPT Deep Research Compatible Tools ---
1845 | 
1846 | def get_preview_text(markdown_content: str, skip_chars: int = 100, preview_chars: int = 200) -> str:
1847 |     """
1848 |     Extract a preview of document text by skipping headers and showing meaningful content.
1849 |     
1850 |     Args:
1851 |         markdown_content: Full document content in markdown format
1852 |         skip_chars: Number of characters to skip from the beginning (default: 100)
1853 |         preview_chars: Number of characters to show in preview (default: 200)
1854 |     
1855 |     Returns:
1856 |         Preview text suitable for ChatGPT Deep Research
1857 |     """
1858 |     if not markdown_content:
1859 |         return ""
1860 |     
1861 |     # Remove common markdown artifacts and clean up
1862 |     cleaned_content = markdown_content.strip()
1863 |     
1864 |     # Skip the first N characters (usually headers, metadata)
1865 |     if len(cleaned_content) > skip_chars:
1866 |         content_start = cleaned_content[skip_chars:]
1867 |     else:
1868 |         content_start = cleaned_content
1869 |     
1870 |     # Get the next N characters for preview
1871 |     if len(content_start) > preview_chars:
1872 |         preview = content_start[:preview_chars]
1873 |     else:
1874 |         preview = content_start
1875 |     
1876 |     # Clean up the preview - remove incomplete sentences at the end
1877 |     preview = preview.strip()
1878 |     
1879 |     # If preview ends mid-sentence, try to end at last complete sentence
1880 |     if preview and not preview.endswith('.'):
1881 |         last_period = preview.rfind('.')
1882 |         if last_period > 50:  # Only if there's a reasonable sentence
1883 |             preview = preview[:last_period + 1]
1884 |     
1885 |     # Add ellipsis if content was truncated
1886 |     if len(content_start) > preview_chars:
1887 |         preview += "..."
1888 |     
1889 |     return preview.strip()
1890 | 
1891 | 
1892 | @app.tool(
1893 |     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",
1894 |     annotations={
1895 |         "readOnlyHint": True,
1896 |         "openWorldHint": True,
1897 |         "idempotentHint": True
1898 |     }
1899 | )
1900 | async def search(
1901 |     query: str = Field(..., description="Turkish search query")
1902 | ) -> Dict[str, Any]:
1903 |     """
1904 |     Bedesten API search tool for ChatGPT Deep Research compatibility.
1905 |     
1906 |     This tool searches Turkish legal databases via the unified Bedesten API.
1907 |     It supports advanced search operators and covers all major court types.
1908 |     
1909 |     USAGE RESTRICTION: Only for ChatGPT Deep Research workflows.
1910 |     For regular legal research, use search_bedesten_unified with specific court types.
1911 |     
1912 |     Returns:
1913 |     Object with "results" field containing a list of documents with id, title, text preview, and url
1914 |     as required by ChatGPT Deep Research specification.
1915 |     """
1916 |     logger.info(f"ChatGPT Deep Research search tool called with query: {query}")
1917 |     
1918 |     results = []
1919 |     
1920 |     try:
1921 |         # Search all court types via unified Bedesten API
1922 |         court_types = [
1923 |             ("YARGITAYKARARI", "Yargıtay", "yargitay_bedesten"),
1924 |             ("DANISTAYKARAR", "Danıştay", "danistay_bedesten"), 
1925 |             ("YERELHUKUK", "Yerel Hukuk Mahkemesi", "yerel_hukuk_bedesten"),
1926 |             ("ISTINAFHUKUK", "İstinaf Hukuk Mahkemesi", "istinaf_hukuk_bedesten"),
1927 |             ("KYB", "Kanun Yararına Bozma", "kyb_bedesten")
1928 |         ]
1929 |         
1930 |         for item_type, court_name, id_prefix in court_types:
1931 |             try:
1932 |                 search_results = await bedesten_client_instance.search_documents(
1933 |                     BedestenSearchRequest(
1934 |                         data=BedestenSearchData(
1935 |                             phrase=query,  # Use query as-is to support both regular and exact phrase searches
1936 |                             itemTypeList=[item_type],
1937 |                             pageSize=10,
1938 |                             pageNumber=1
1939 |                         )
1940 |                     )
1941 |                 )
1942 |                 
1943 |                 # Handle potential None data
1944 |                 if search_results.data is None:
1945 |                     logger.warning(f"No data returned from Bedesten API for {court_name}")
1946 |                     continue
1947 |                 
1948 |                 # Add results from this court type (limit to top 5 per court)
1949 |                 for decision in search_results.data.emsalKararList[:5]:
1950 |                     # For ChatGPT Deep Research, fetch document content for preview
1951 |                     try:
1952 |                         # Fetch document content for preview
1953 |                         doc = await bedesten_client_instance.get_document_as_markdown(decision.documentId)
1954 |                         
1955 |                         # Generate preview text (skip first 100 chars, show next 200)
1956 |                         preview_text = get_preview_text(doc.markdown_content, skip_chars=100, preview_chars=200)
1957 |                         
1958 |                         # Build title from metadata
1959 |                         title_parts = []
1960 |                         if decision.birimAdi:
1961 |                             title_parts.append(decision.birimAdi)
1962 |                         if decision.esasNo:
1963 |                             title_parts.append(f"Esas: {decision.esasNo}")
1964 |                         if decision.kararNo:
1965 |                             title_parts.append(f"Karar: {decision.kararNo}")
1966 |                         if decision.kararTarihiStr:
1967 |                             title_parts.append(f"Tarih: {decision.kararTarihiStr}")
1968 |                         
1969 |                         if title_parts:
1970 |                             title = " - ".join(title_parts)
1971 |                         else:
1972 |                             title = f"{court_name} - Document {decision.documentId}"
1973 |                         
1974 |                         # Add to results in OpenAI format
1975 |                         results.append({
1976 |                             "id": decision.documentId,
1977 |                             "title": title,
1978 |                             "text": preview_text,
1979 |                             "url": f"https://mevzuat.adalet.gov.tr/ictihat/{decision.documentId}"
1980 |                         })
1981 |                         
1982 |                     except Exception as e:
1983 |                         logger.warning(f"Could not fetch preview for document {decision.documentId}: {e}")
1984 |                         # Add minimal result without preview
1985 |                         results.append({
1986 |                             "id": decision.documentId,
1987 |                             "title": f"{court_name} - Document {decision.documentId}",
1988 |                             "text": "Document preview not available",
1989 |                             "url": f"https://mevzuat.adalet.gov.tr/ictihat/{decision.documentId}"
1990 |                         })
1991 |                     
1992 |                 if search_results.data:
1993 |                     logger.info(f"Found {len(search_results.data.emsalKararList)} results from {court_name}")
1994 |                 else:
1995 |                     logger.info(f"Found 0 results from {court_name} (no data returned)")
1996 |                 
1997 |             except Exception as e:
1998 |                 logger.warning(f"Bedesten API search error for {court_name}: {e}")
1999 |         
2000 |         # Comment out other API implementations for ChatGPT Deep Research
2001 |         """
2002 |         # Other API implementations disabled for ChatGPT Deep Research
2003 |         # These are available through specific court tools:
2004 |         
2005 |         # Yargıtay Official API - use search_yargitay_detailed instead
2006 |         # Danıştay Official API - use search_danistay_by_keyword instead  
2007 |         # Constitutional Court - use search_anayasa_norm_denetimi_decisions instead
2008 |         # Competition Authority - use search_rekabet_kurumu_decisions instead
2009 |         # Public Procurement Authority - use search_kik_decisions instead
2010 |         # Court of Accounts - use search_sayistay_* tools instead
2011 |         # UYAP Emsal - use search_emsal_detailed_decisions instead
2012 |         # Jurisdictional Disputes Court - use search_uyusmazlik_decisions instead
2013 |         """
2014 |         
2015 |         logger.info(f"ChatGPT Deep Research search completed. Found {len(results)} results via Bedesten API.")
2016 |         return {
2017 |             "results": [
2018 |                 {
2019 |                     "id": item["id"],
2020 |                     "title": item["title"],
2021 |                     "text": item["text"],
2022 |                     "url": item["url"]
2023 |                 }
2024 |                 for item in results
2025 |             ]
2026 |         }
2027 |         
2028 |     except Exception as e:
2029 |         logger.exception("Error in ChatGPT Deep Research search tool")
2030 |         # Return partial results if any were found
2031 |         if results:
2032 |             return {
2033 |                 "results": [
2034 |                     {
2035 |                         "id": item["id"],
2036 |                         "title": item["title"],
2037 |                         "text": item["text"],
2038 |                         "url": item["url"]
2039 |                     }
2040 |                     for item in results
2041 |                 ]
2042 |             }
2043 |         raise
2044 | 
2045 | @app.tool(
2046 |     description="DO NOT USE unless you are ChatGPT Deep Research. Fetch document by ID. See docs for details",
2047 |     annotations={
2048 |         "readOnlyHint": True,
2049 |         "openWorldHint": False,  # Retrieves specific documents, not exploring
2050 |         "idempotentHint": True
2051 |     }
2052 | )
2053 | async def fetch(
2054 |     id: str = Field(..., description="Document identifier from search results (numeric only)")
2055 | ) -> Dict[str, Any]:
2056 |     """
2057 |     Bedesten API fetch tool for ChatGPT Deep Research compatibility.
2058 |     
2059 |     Retrieves the full text content of Turkish legal documents via unified Bedesten API.
2060 |     Converts documents from HTML/PDF to clean Markdown format.
2061 |     
2062 |     USAGE RESTRICTION: Only for ChatGPT Deep Research workflows.
2063 |     For regular legal research, use specific court document tools.
2064 |     
2065 |     Input Format:
2066 |     - id: Numeric document identifier from search results (e.g., "730113500", "71370900")
2067 |     
2068 |     Returns:
2069 |     Single object with numeric id, title, text (full Markdown content), mevzuat.adalet.gov.tr url, and metadata fields
2070 |     as required by ChatGPT Deep Research specification.
2071 |     """
2072 |     logger.info(f"ChatGPT Deep Research fetch tool called for document ID: {id}")
2073 |     
2074 |     if not id or not id.strip():
2075 |         raise ValueError("Document ID must be a non-empty string")
2076 |     
2077 |     try:
2078 |         # Use the numeric ID directly with Bedesten API
2079 |         doc = await bedesten_client_instance.get_document_as_markdown(id)
2080 |         
2081 |         # Try to get additional metadata by searching for this specific document
2082 |         title = f"Turkish Legal Document {id}"
2083 |         try:
2084 |             # Quick search to get metadata for better title
2085 |             search_results = await bedesten_client_instance.search_documents(
2086 |                 BedestenSearchRequest(
2087 |                     data=BedestenSearchData(
2088 |                         phrase=id,  # Search by document ID
2089 |                         pageSize=1,
2090 |                         pageNumber=1
2091 |                     )
2092 |                 )
2093 |             )
2094 |             
2095 |             if search_results.data and search_results.data.emsalKararList:
2096 |                 decision = search_results.data.emsalKararList[0]
2097 |                 if decision.documentId == id:
2098 |                     # Build a proper title from metadata
2099 |                     title_parts = []
2100 |                     if decision.birimAdi:
2101 |                         title_parts.append(decision.birimAdi)
2102 |                     if decision.esasNo:
2103 |                         title_parts.append(f"Esas: {decision.esasNo}")
2104 |                     if decision.kararNo:
2105 |                         title_parts.append(f"Karar: {decision.kararNo}")
2106 |                     if decision.kararTarihiStr:
2107 |                         title_parts.append(f"Tarih: {decision.kararTarihiStr}")
2108 |                     
2109 |                     if title_parts:
2110 |                         title = " - ".join(title_parts)
2111 |                     else:
2112 |                         title = f"Turkish Legal Decision {id}"
2113 |         except Exception as e:
2114 |             logger.warning(f"Could not fetch metadata for document {id}: {e}")
2115 |         
2116 |         return {
2117 |             "id": id,
2118 |             "title": title,
2119 |             "text": doc.markdown_content,
2120 |             "url": f"https://mevzuat.adalet.gov.tr/ictihat/{id}",
2121 |             "metadata": {
2122 |                 "database": "Turkish Legal Database via Bedesten API",
2123 |                 "document_id": id,
2124 |                 "source_url": doc.source_url,
2125 |                 "mime_type": doc.mime_type,
2126 |                 "api_source": "Bedesten Unified API",
2127 |                 "chatgpt_deep_research": True
2128 |             }
2129 |         }
2130 |         
2131 |         # Comment out other API implementations for ChatGPT Deep Research
2132 |         """
2133 |         # Other API implementations disabled for ChatGPT Deep Research
2134 |         # These are available through specific court document tools:
2135 |         
2136 |         elif id.startswith("yargitay_"):
2137 |             # Yargıtay Official API - use get_yargitay_document_markdown instead
2138 |             doc_id = id.replace("yargitay_", "")
2139 |             doc = await yargitay_client_instance.get_decision_document_as_markdown(doc_id)
2140 |             
2141 |         elif id.startswith("danistay_"):
2142 |             # Danıştay Official API - use get_danistay_document_markdown instead
2143 |             doc_id = id.replace("danistay_", "")
2144 |             doc = await danistay_client_instance.get_decision_document_as_markdown(doc_id)
2145 |             
2146 |         elif id.startswith("anayasa_"):
2147 |             # Constitutional Court - use get_anayasa_norm_denetimi_document_markdown instead
2148 |             doc_id = id.replace("anayasa_", "")
2149 |             doc = await anayasa_norm_client_instance.get_decision_document_as_markdown(...)
2150 |             
2151 |         elif id.startswith("rekabet_"):
2152 |             # Competition Authority - use get_rekabet_kurumu_document instead
2153 |             doc_id = id.replace("rekabet_", "")
2154 |             doc = await rekabet_client_instance.get_decision_document(...)
2155 |             
2156 |         elif id.startswith("kik_"):
2157 |             # Public Procurement Authority - use get_kik_decision_document_as_markdown instead
2158 |             doc_id = id.replace("kik_", "")
2159 |             doc = await kik_client_instance.get_decision_document_as_markdown(doc_id)
2160 |             
2161 |         elif id.startswith("local_"):
2162 |             # This was already using Bedesten API, but deprecated for ChatGPT Deep Research
2163 |             doc_id = id.replace("local_", "")
2164 |             doc = await bedesten_client_instance.get_document_as_markdown(doc_id)
2165 |         """
2166 |         
2167 |     except Exception as e:
2168 |         logger.exception(f"Error fetching ChatGPT Deep Research document {id}")
2169 |         raise
2170 | 
2171 | # --- Token Metrics Tool Removed for Optimization ---
2172 | 
2173 | def ensure_playwright_browsers():
2174 |     """Ensure Playwright browsers are installed for KIK tool functionality."""
2175 |     try:
2176 |         import subprocess
2177 |         import os
2178 |         
2179 |         # Check if chromium is already installed
2180 |         chromium_path = os.path.expanduser("~/Library/Caches/ms-playwright/chromium-1179")
2181 |         if os.path.exists(chromium_path):
2182 |             logger.info("Playwright Chromium browser already installed.")
2183 |             return
2184 |         
2185 |         logger.info("Installing Playwright Chromium browser for KIK tool...")
2186 |         result = subprocess.run(
2187 |             ["python", "-m", "playwright", "install", "chromium"],
2188 |             capture_output=True,
2189 |             text=True,
2190 |             timeout=300  # 5 minutes timeout
2191 |         )
2192 |         
2193 |         if result.returncode == 0:
2194 |             logger.info("Playwright Chromium browser installed successfully.")
2195 |         else:
2196 |             logger.warning(f"Failed to install Playwright browser: {result.stderr}")
2197 |             logger.warning("KIK tool may not work properly without Playwright browsers.")
2198 |             
2199 |     except Exception as e:
2200 |         logger.warning(f"Could not auto-install Playwright browsers: {e}")
2201 |         logger.warning("KIK tool may not work properly. Manual installation: 'playwright install chromium'")
2202 | 
2203 | def main():
2204 |     # Initialize the app properly with create_app()
2205 |     global app
2206 |     app = create_app()
2207 |     
2208 |     logger.info(f"Starting {app.name} server via main() function...")
2209 |     # logger.info(f"Logs will be written to: {LOG_FILE_PATH}")  # File logging disabled
2210 |     
2211 |     # Ensure Playwright browsers are installed
2212 |     ensure_playwright_browsers()
2213 |     
2214 |     try:
2215 |         app.run()
2216 |     except KeyboardInterrupt: 
2217 |         logger.info("Server shut down by user (KeyboardInterrupt).")
2218 |     except Exception as e: 
2219 |         logger.exception("Server failed to start or crashed.")
2220 |     finally:
2221 |         logger.info(f"{app.name} server has shut down.")
2222 | 
2223 | if __name__ == "__main__": 
2224 |     main()
```
Page 10/11FirstPrevNextLast