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