optimize token usage

This commit is contained in:
saidsurucu
2025-07-14 15:45:19 +03:00
parent 54e8d61f83
commit e34d81be26
11 changed files with 489 additions and 277 deletions
+326 -129
View File
@@ -4,9 +4,14 @@ import atexit
import logging
import os
import httpx
import json
import time
from collections import defaultdict
from pydantic import HttpUrl, Field
from typing import Optional, Dict, List, Literal, Any, Union
import urllib.parse
import tiktoken
from fastmcp.server.middleware import Middleware, MiddlewareContext
# --- Logging Configuration Start ---
LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
@@ -32,6 +37,197 @@ root_logger.addHandler(console_handler)
logger = logging.getLogger(__name__)
# --- Logging Configuration End ---
# --- Token Counting Middleware ---
class TokenCountingMiddleware(Middleware):
"""Middleware for counting input/output tokens using tiktoken."""
def __init__(self, model: str = "cl100k_base"):
"""Initialize token counting middleware.
Args:
model: Tiktoken model name (cl100k_base for GPT-4/Claude compatibility)
"""
self.encoder = tiktoken.get_encoding(model)
self.model = model
self.token_stats = defaultdict(lambda: {"input": 0, "output": 0, "calls": 0})
self.logger = logging.getLogger("token_counter")
# Create separate log file for token metrics
token_log_path = os.path.join(LOG_DIRECTORY, "token_metrics.log")
token_handler = logging.FileHandler(token_log_path, mode='a', encoding='utf-8')
token_formatter = logging.Formatter('%(asctime)s - %(message)s')
token_handler.setFormatter(token_formatter)
token_handler.setLevel(logging.INFO)
self.logger.addHandler(token_handler)
self.logger.setLevel(logging.INFO)
def count_tokens(self, text: str) -> int:
"""Count tokens in text using tiktoken."""
if not text:
return 0
try:
return len(self.encoder.encode(str(text)))
except Exception as e:
logger.warning(f"Token counting failed: {e}")
return 0
def extract_text_content(self, data: Any) -> str:
"""Extract text content from various data types."""
if isinstance(data, str):
return data
elif isinstance(data, dict):
# Extract text from common response fields
text_parts = []
for key, value in data.items():
if isinstance(value, str):
text_parts.append(value)
elif isinstance(value, list):
for item in value:
if isinstance(item, str):
text_parts.append(item)
elif isinstance(item, dict) and 'text' in item:
text_parts.append(str(item['text']))
return ' '.join(text_parts)
elif isinstance(data, list):
text_parts = []
for item in data:
text_parts.append(self.extract_text_content(item))
return ' '.join(text_parts)
else:
return str(data)
def log_token_usage(self, operation: str, input_tokens: int, output_tokens: int,
tool_name: str = None, duration_ms: float = None):
"""Log token usage with structured format."""
log_data = {
"operation": operation,
"tool_name": tool_name,
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"total_tokens": input_tokens + output_tokens,
"duration_ms": duration_ms,
"timestamp": time.time()
}
# Update statistics
key = tool_name if tool_name else operation
self.token_stats[key]["input"] += input_tokens
self.token_stats[key]["output"] += output_tokens
self.token_stats[key]["calls"] += 1
# Log as JSON for easy parsing
self.logger.info(json.dumps(log_data))
# Also log human-readable format to main logger
logger.info(f"Token Usage - {operation}" +
(f" ({tool_name})" if tool_name else "") +
f": {input_tokens} in + {output_tokens} out = {input_tokens + output_tokens} total")
async def on_call_tool(self, context: MiddlewareContext, call_next):
"""Count tokens for tool calls."""
start_time = time.perf_counter()
# Extract tool name and arguments
tool_name = getattr(context.message, 'name', 'unknown_tool')
tool_args = getattr(context.message, 'arguments', {})
# Count input tokens (tool arguments)
input_text = self.extract_text_content(tool_args)
input_tokens = self.count_tokens(input_text)
try:
# Execute the tool
result = await call_next(context)
# Count output tokens (tool result)
output_text = self.extract_text_content(result)
output_tokens = self.count_tokens(output_text)
# Calculate duration
duration_ms = (time.perf_counter() - start_time) * 1000
# Log token usage
self.log_token_usage("tool_call", input_tokens, output_tokens,
tool_name, duration_ms)
return result
except Exception as e:
duration_ms = (time.perf_counter() - start_time) * 1000
self.log_token_usage("tool_call_error", input_tokens, 0,
tool_name, duration_ms)
raise
async def on_read_resource(self, context: MiddlewareContext, call_next):
"""Count tokens for resource reads."""
start_time = time.perf_counter()
# Extract resource URI
resource_uri = getattr(context.message, 'uri', 'unknown_resource')
try:
# Execute the resource read
result = await call_next(context)
# Count output tokens (resource content)
output_text = self.extract_text_content(result)
output_tokens = self.count_tokens(output_text)
# Calculate duration
duration_ms = (time.perf_counter() - start_time) * 1000
# Log token usage (no input tokens for resource reads)
self.log_token_usage("resource_read", 0, output_tokens,
resource_uri, duration_ms)
return result
except Exception as e:
duration_ms = (time.perf_counter() - start_time) * 1000
self.log_token_usage("resource_read_error", 0, 0,
resource_uri, duration_ms)
raise
async def on_get_prompt(self, context: MiddlewareContext, call_next):
"""Count tokens for prompt retrievals."""
start_time = time.perf_counter()
# Extract prompt name
prompt_name = getattr(context.message, 'name', 'unknown_prompt')
try:
# Execute the prompt retrieval
result = await call_next(context)
# Count output tokens (prompt content)
output_text = self.extract_text_content(result)
output_tokens = self.count_tokens(output_text)
# Calculate duration
duration_ms = (time.perf_counter() - start_time) * 1000
# Log token usage
self.log_token_usage("prompt_get", 0, output_tokens,
prompt_name, duration_ms)
return result
except Exception as e:
duration_ms = (time.perf_counter() - start_time) * 1000
self.log_token_usage("prompt_get_error", 0, 0,
prompt_name, duration_ms)
raise
def get_token_stats(self) -> Dict[str, Any]:
"""Get current token usage statistics."""
return dict(self.token_stats)
def reset_token_stats(self):
"""Reset token usage statistics."""
self.token_stats.clear()
# --- End Token Counting Middleware ---
# Create FastMCP app directly without authentication wrapper
from fastmcp import FastMCP
@@ -113,6 +309,11 @@ from kvkk_mcp_module.models import (
app = create_app()
# --- Add Token Counting Middleware ---
token_counter = TokenCountingMiddleware()
app.add_middleware(token_counter)
logger.info("Token counting middleware added to MCP server")
# --- Tool Documentation Resources ---
@app.resource("docs://tools/yargitay")
async def get_yargitay_tools_documentation() -> str:
@@ -863,7 +1064,7 @@ KARAR_TURU_ADI_TO_GUID_ENUM_MAP = {
# --- MCP Tools for Yargitay ---
@app.tool(
description="Search Yargıtay decisions using primary API with 52 chamber filtering and advanced operators. Before using, read docs://tools/yargitay",
description="Search Yargıtay decisions with 52 chamber filtering and advanced operators",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -871,10 +1072,8 @@ KARAR_TURU_ADI_TO_GUID_ENUM_MAP = {
}
)
async def search_yargitay_detailed(
arananKelime: str = Field("", description="Search keyword with OR/AND/wildcard operators"),
arananKelime: str = Field("", description="Turkish search keyword. Supports +required -excluded \"exact phrase\" operators"),
birimYrgKurulDaire: str = Field("ALL", description="Chamber selection (52 options: Civil/Criminal chambers, General Assemblies)"),
birimYrgHukukDaire: str = Field("", description="Legacy field - use birimYrgKurulDaire instead"),
birimYrgCezaDaire: str = Field("", description="Legacy field - use birimYrgKurulDaire instead"),
esasYil: str = Field("", description="Case year for 'Esas No'."),
esasIlkSiraNo: str = Field("", description="Starting sequence number for 'Esas No'."),
esasSonSiraNo: str = Field("", description="Ending sequence number for 'Esas No'."),
@@ -883,8 +1082,6 @@ async def search_yargitay_detailed(
kararSonSiraNo: str = Field("", description="Ending sequence number for 'Karar No'."),
baslangicTarihi: str = Field("", description="Start date for decision search (DD.MM.YYYY)."),
bitisTarihi: str = Field("", description="End date for decision search (DD.MM.YYYY)."),
siralama: str = Field("3", description="Sorting criteria (1: Esas No, 2: Karar No, 3: Karar Tarihi)."),
siralamaDirection: str = Field("desc", description="Sorting direction ('asc' or 'desc')."),
pageSize: int = Field(10, ge=1, le=10, description="Number of results per page."),
pageNumber: int = Field(1, ge=1, description="Page number to retrieve.")
) -> CompactYargitaySearchResult:
@@ -897,8 +1094,6 @@ async def search_yargitay_detailed(
search_query = YargitayDetailedSearchRequest(
arananKelime=arananKelime,
birimYrgKurulDaire=birimYrgKurulDaire,
birimYrgHukukDaire=birimYrgHukukDaire,
birimYrgCezaDaire=birimYrgCezaDaire,
esasYil=esasYil,
esasIlkSiraNo=esasIlkSiraNo,
esasSonSiraNo=esasSonSiraNo,
@@ -907,8 +1102,8 @@ async def search_yargitay_detailed(
kararSonSiraNo=kararSonSiraNo,
baslangicTarihi=baslangicTarihi,
bitisTarihi=bitisTarihi,
siralama=siralama,
siralamaDirection=siralamaDirection,
siralama="3",
siralamaDirection="desc",
pageSize=pageSize,
pageNumber=pageNumber
)
@@ -941,7 +1136,7 @@ async def search_yargitay_detailed(
raise
@app.tool(
description="Retrieve full text of a Yargıtay decision in Markdown format. Before using, read docs://tools/yargitay",
description="Get Yargıtay decision text in Markdown format",
annotations={
"readOnlyHint": True,
"idempotentHint": True
@@ -959,7 +1154,7 @@ async def get_yargitay_document_markdown(id: str) -> YargitayDocumentMarkdown:
# --- MCP Tools for Danistay ---
@app.tool(
description="Search Danıştay decisions using keyword logic with AND/OR/NOT operators. Before using, read docs://tools/danistay",
description="Search Danıştay decisions with keyword logic (AND/OR/NOT operators)",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -1001,7 +1196,7 @@ async def search_danistay_by_keyword(
raise
@app.tool(
description="Search Danıştay decisions with detailed criteria including chamber selection and case numbers. Before using, read docs://tools/danistay",
description="Search Danıştay decisions with detailed criteria (chamber selection, case numbers)",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -1009,20 +1204,18 @@ async def search_danistay_by_keyword(
}
)
async def search_danistay_detailed(
daire: Optional[str] = Field(None, description="Chamber/Department name (e.g., '1. Daire')."),
esasYil: Optional[str] = Field(None, description="Case year for 'Esas No'."),
esasIlkSiraNo: Optional[str] = Field(None, description="Starting sequence for 'Esas No'."),
esasSonSiraNo: Optional[str] = Field(None, description="Ending sequence for 'Esas No'."),
kararYil: Optional[str] = Field(None, description="Decision year for 'Karar No'."),
kararIlkSiraNo: Optional[str] = Field(None, description="Starting sequence for 'Karar No'."),
kararSonSiraNo: Optional[str] = Field(None, description="Ending sequence for 'Karar No'."),
baslangicTarihi: Optional[str] = Field(None, description="Start date for decision (DD.MM.YYYY)."),
bitisTarihi: Optional[str] = Field(None, description="End date for decision (DD.MM.YYYY)."),
mevzuatNumarasi: Optional[str] = Field(None, description="Legislation number."),
mevzuatAdi: Optional[str] = Field(None, description="Legislation name."),
madde: Optional[str] = Field(None, description="Article number."),
siralama: str = Field("1", description="Sorting criteria (e.g., 1: Esas No, 3: Karar Tarihi)."),
siralamaDirection: str = Field("desc", description="Sorting direction ('asc' or 'desc')."),
daire: str = Field("", description="Chamber/Department name (e.g., '1. Daire')."),
esasYil: str = Field("", description="Case year for 'Esas No'."),
esasIlkSiraNo: str = Field("", description="Starting sequence for 'Esas No'."),
esasSonSiraNo: str = Field("", description="Ending sequence for 'Esas No'."),
kararYil: str = Field("", description="Decision year for 'Karar No'."),
kararIlkSiraNo: str = Field("", description="Starting sequence for 'Karar No'."),
kararSonSiraNo: str = Field("", description="Ending sequence for 'Karar No'."),
baslangicTarihi: str = Field("", description="Start date for decision (DD.MM.YYYY)."),
bitisTarihi: str = Field("", description="End date for decision (DD.MM.YYYY)."),
mevzuatNumarasi: str = Field("", description="Legislation number."),
mevzuatAdi: str = Field("", description="Legislation name."),
madde: str = Field("", description="Article number."),
pageNumber: int = Field(1, ge=1, description="Page number."),
pageSize: int = Field(10, ge=1, le=10, description="Results per page.")
) -> CompactDanistaySearchResult:
@@ -1041,8 +1234,8 @@ async def search_danistay_detailed(
mevzuatNumarasi=mevzuatNumarasi,
mevzuatAdi=mevzuatAdi,
madde=madde,
siralama=siralama,
siralamaDirection=siralamaDirection,
siralama="3",
siralamaDirection="desc",
pageNumber=pageNumber,
pageSize=pageSize
)
@@ -1063,7 +1256,7 @@ async def search_danistay_detailed(
raise
@app.tool(
description="Retrieve full text of a Danıştay decision in Markdown format. Before using, read docs://tools/danistay",
description="Get Danıştay decision text in Markdown format",
annotations={
"readOnlyHint": True,
"idempotentHint": True
@@ -1081,7 +1274,7 @@ async def get_danistay_document_markdown(id: str) -> DanistayDocumentMarkdown:
# --- MCP Tools for Emsal ---
@app.tool(
description="Search Emsal precedent decisions with detailed criteria across Turkish courts. Before using, read docs://tools/emsal",
description="Search Emsal precedent decisions with detailed criteria",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -1089,18 +1282,18 @@ async def get_danistay_document_markdown(id: str) -> DanistayDocumentMarkdown:
}
)
async def search_emsal_detailed_decisions(
keyword: Optional[str] = Field(None, description="Keyword to search."),
selected_bam_civil_court: Optional[str] = Field(None, description="Selected BAM Civil Court."),
selected_civil_court: Optional[str] = Field(None, description="Selected Civil Court."),
keyword: str = Field("", description="Keyword to search."),
selected_bam_civil_court: str = Field("", description="Selected BAM Civil Court."),
selected_civil_court: str = Field("", description="Selected Civil Court."),
selected_regional_civil_chambers: List[str] = Field(default_factory=list, description="Selected Regional Civil Chambers."),
case_year_esas: Optional[str] = Field(None, description="Case year for 'Esas No'."),
case_start_seq_esas: Optional[str] = Field(None, description="Starting sequence for 'Esas No'."),
case_end_seq_esas: Optional[str] = Field(None, description="Ending sequence for 'Esas No'."),
decision_year_karar: Optional[str] = Field(None, description="Decision year for 'Karar No'."),
decision_start_seq_karar: Optional[str] = Field(None, description="Starting sequence for 'Karar No'."),
decision_end_seq_karar: Optional[str] = Field(None, description="Ending sequence for 'Karar No'."),
start_date: Optional[str] = Field(None, description="Start date for decision (DD.MM.YYYY)."),
end_date: Optional[str] = Field(None, description="End date for decision (DD.MM.YYYY)."),
case_year_esas: str = Field("", description="Case year for 'Esas No'."),
case_start_seq_esas: str = Field("", description="Starting sequence for 'Esas No'."),
case_end_seq_esas: str = Field("", description="Ending sequence for 'Esas No'."),
decision_year_karar: str = Field("", description="Decision year for 'Karar No'."),
decision_start_seq_karar: str = Field("", description="Starting sequence for 'Karar No'."),
decision_end_seq_karar: str = Field("", description="Ending sequence for 'Karar No'."),
start_date: str = Field("", description="Start date for decision (DD.MM.YYYY)."),
end_date: str = Field("", description="End date for decision (DD.MM.YYYY)."),
sort_criteria: str = Field("1", description="Sorting criteria (e.g., 1: Esas No)."),
sort_direction: str = Field("desc", description="Sorting direction ('asc' or 'desc')."),
page_number: int = Field(1, ge=1, description="Page number (accepts int)."),
@@ -1144,7 +1337,7 @@ async def search_emsal_detailed_decisions(
raise
@app.tool(
description="Retrieve full text of an Emsal precedent decision in Markdown format. Before using, read docs://tools/emsal",
description="Get Emsal precedent decision text in Markdown format",
annotations={
"readOnlyHint": True,
"idempotentHint": True
@@ -1162,7 +1355,7 @@ async def get_emsal_document_markdown(id: str) -> EmsalDocumentMarkdown:
# --- MCP Tools for Uyusmazlik ---
@app.tool(
description="Search Uyuşmazlık Mahkemesi decisions for jurisdictional disputes between court systems. Before using, read docs://tools/uyusmazlik",
description="Search Uyuşmazlık Mahkemesi decisions for jurisdictional disputes",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -1233,7 +1426,7 @@ async def search_uyusmazlik_decisions(
raise
@app.tool(
description="Retrieve full text of an Uyuşmazlık Mahkemesi decision from URL in Markdown format. Before using, read docs://tools/uyusmazlik",
description="Get Uyuşmazlık Mahkemesi decision text from URL in Markdown format",
annotations={
"readOnlyHint": True,
"idempotentHint": True
@@ -1254,7 +1447,7 @@ async def get_uyusmazlik_document_markdown_from_url(
# --- MCP Tools for Anayasa Mahkemesi (Norm Denetimi) ---
@app.tool(
description="Search Constitutional Court norm control decisions with comprehensive filtering and legal criteria. Before using, read docs://tools/constitutional_court",
description="Search Constitutional Court norm control decisions with comprehensive filtering",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -1262,34 +1455,34 @@ async def get_uyusmazlik_document_markdown_from_url(
}
)
async def search_anayasa_norm_denetimi_decisions(
keywords_all: List[str] = Field(default_factory=list, description="AND keywords"),
keywords_any: List[str] = Field(default_factory=list, description="OR keywords"),
keywords_exclude: List[str] = Field(default_factory=list, description="Exclude keywords"),
keywords_all: List[str] = Field(default_factory=list, description="AND"),
keywords_any: List[str] = Field(default_factory=list, description="OR"),
keywords_exclude: List[str] = Field(default_factory=list, description="NOT"),
period: Literal["ALL", "1", "2"] = Field("ALL", description="Period"),
case_number_esas: Optional[str] = Field(None, description="Case number"),
decision_number_karar: Optional[str] = Field(None, description="Decision number"),
first_review_date_start: Optional[str] = Field(None, description="Review start date"),
first_review_date_end: Optional[str] = Field(None, description="Review end date"),
decision_date_start: Optional[str] = Field(None, description="Decision start"),
decision_date_end: Optional[str] = Field(None, description="Decision end"),
case_number_esas: str = Field("", description="Case number"),
decision_number_karar: str = Field("", description="Decision number"),
first_review_date_start: str = Field("", description="Start"),
first_review_date_end: str = Field("", description="End"),
decision_date_start: str = Field("", description="Start"),
decision_date_end: str = Field("", description="End"),
application_type: Literal["ALL", "1", "2", "3"] = Field("ALL", description="App type"),
applicant_general_name: Optional[str] = Field(None, description="Applicant"),
applicant_specific_name: Optional[str] = Field(None, description="Specific name"),
official_gazette_date_start: Optional[str] = Field(None, description="Gazette start"),
official_gazette_date_end: Optional[str] = Field(None, description="Gazette end"),
official_gazette_number_start: Optional[str] = Field(None, description="Gazette no start"),
official_gazette_number_end: Optional[str] = Field(None, description="Gazette no end"),
has_press_release: Literal["ALL", "0", "1"] = Field("ALL", description="Press release"),
applicant_general_name: str = Field("", description="Applicant"),
applicant_specific_name: str = Field("", description="Name"),
official_gazette_date_start: str = Field("", description="Start"),
official_gazette_date_end: str = Field("", description="End"),
official_gazette_number_start: str = Field("", description="Start"),
official_gazette_number_end: str = Field("", description="End"),
has_press_release: Literal["ALL", "0", "1"] = Field("ALL", description="Press"),
has_dissenting_opinion: Literal["ALL", "0", "1"] = Field("ALL", description="Dissenting"),
has_different_reasoning: Literal["ALL", "0", "1"] = Field("ALL", description="Different reason"),
has_different_reasoning: Literal["ALL", "0", "1"] = Field("ALL", description="Reason"),
attending_members_names: List[str] = Field(default_factory=list, description="Members"),
rapporteur_name: Optional[str] = Field(None, description="Rapporteur"),
rapporteur_name: str = Field("", description="Rapporteur"),
norm_type: Literal["ALL", "1", "2", "14", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "0", "13"] = Field("ALL", description="Norm type"),
norm_id_or_name: Optional[str] = Field(None, description="Norm name"),
norm_article: Optional[str] = Field(None, description="Article"),
norm_id_or_name: str = Field("", description="Norm name"),
norm_article: str = Field("", description="Article"),
review_outcomes: List[Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8", "12"]] = Field(default_factory=list, description="Outcomes"),
reason_for_final_outcome: Literal["ALL", "29", "1", "2", "30", "3", "4", "27", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26"] = Field("ALL", description="Reason"),
basis_constitution_article_numbers: List[str] = Field(default_factory=list, description="Articles"),
basis_constitution_article_numbers: List[str] = Field(default_factory=list, description="Arts"),
results_per_page: int = Field(10, description="Count"),
page_to_fetch: int = Field(1, ge=1, description="Page"),
sort_by_criteria: str = Field("KararTarihi", description="Sort")
@@ -1348,7 +1541,7 @@ async def search_anayasa_norm_denetimi_decisions(
raise
@app.tool(
description="Retrieve full text of a Constitutional Court norm control decision in paginated Markdown format. Before using, read docs://tools/constitutional_court",
description="Get Constitutional Court norm control decision text in paginated Markdown format",
annotations={
"readOnlyHint": True,
"idempotentHint": True
@@ -1371,7 +1564,7 @@ async def get_anayasa_norm_denetimi_document_markdown(
# --- MCP Tools for Anayasa Mahkemesi (Bireysel Başvuru Karar Raporu & Belgeler) ---
@app.tool(
description="Search Constitutional Court individual application decisions for human rights violation reports. Before using, read docs://tools/constitutional_court",
description="Search Constitutional Court individual application decisions for human rights violations",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -1397,7 +1590,7 @@ async def search_anayasa_bireysel_basvuru_report(
raise
@app.tool(
description="Parameter description",
description="Get Constitutional Court individual application decision text in paginated Markdown format",
annotations={
"readOnlyHint": True,
"idempotentHint": True
@@ -1427,7 +1620,7 @@ async def get_anayasa_bireysel_basvuru_document_markdown(
# --- MCP Tools for KIK (Kamu İhale Kurulu) ---
@app.tool(
description="Search Public Procurement Authority (KİK) decisions for procurement law and administrative disputes. Before using, read docs://tools/kik",
description="Search Public Procurement Authority (KİK) decisions for procurement law disputes",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -1436,21 +1629,23 @@ async def get_anayasa_bireysel_basvuru_document_markdown(
)
async def search_kik_decisions(
karar_tipi: Literal["rbUyusmazlik", "rbDuzenleyici", "rbMahkeme"] = Field("rbUyusmazlik", description="Type of KIK Decision."),
karar_no: Optional[str] = Field(None, description="Decision Number (e.g., '2024/UH.II-1766')."),
karar_tarihi_baslangic: Optional[str] = Field(None, description="Decision Date Start (DD.MM.YYYY)."),
karar_tarihi_bitis: Optional[str] = Field(None, description="Decision Date End (DD.MM.YYYY)."),
basvuru_sahibi: Optional[str] = Field(None, description="Applicant."),
ihaleyi_yapan_idare: Optional[str] = Field(None, description="Procuring Entity."),
basvuru_konusu_ihale: Optional[str] = Field(None, description="Tender subject of the application."),
karar_metni: Optional[str] = Field(None, description="""
karar_no: str = Field("", description="Decision Number (e.g., '2024/UH.II-1766')."),
karar_tarihi_baslangic: str = Field("", description="Decision Date Start (DD.MM.YYYY)."),
karar_tarihi_bitis: str = Field("", description="Decision Date End (DD.MM.YYYY)."),
basvuru_sahibi: str = Field("", description="Applicant."),
ihaleyi_yapan_idare: str = Field("", description="Procuring Entity."),
basvuru_konusu_ihale: str = Field("", description="Tender subject of the application."),
karar_metni: str = Field("", description="""
Keyword/phrase in decision text. Advanced search operators supported:
- word1+word2 = AND logic (+anayasa +mahkeme -> both words required)
- +"required" -"excluded" = Include and exclude (+ihale -"iptal")
Examples: anayasa | +anayasa +mahkeme | +ihale -"iptal"
- "exact phrase" = Exact match ("kamu yararı" for exact phrase)
- OR/AND/wildcard operators supported
Examples: anayasa | +anayasa +mahkeme | +ihale -"iptal" | "tam cümle"
"""),
yil: Optional[str] = Field(None, description="Year of the decision."),
resmi_gazete_tarihi: Optional[str] = Field(None, description="Official Gazette Date (DD.MM.YYYY)."),
resmi_gazete_sayisi: Optional[str] = Field(None, description="Official Gazette Number."),
yil: str = Field("", description="Year of the decision."),
resmi_gazete_tarihi: str = Field("", description="Official Gazette Date (DD.MM.YYYY)."),
resmi_gazete_sayisi: str = Field("", description="Official Gazette Number."),
page: int = Field(1, ge=1, description="Results page number.")
) -> KikSearchResult:
"""Search Public Procurement Authority (KIK) decisions."""
@@ -1486,7 +1681,7 @@ async def search_kik_decisions(
return KikSearchResult(decisions=[], total_records=0, current_page=current_page_val)
@app.tool(
description="Retrieve full text of a Public Procurement Authority (KİK) decision in paginated Markdown format. Before using, read docs://tools/kik",
description="Get Public Procurement Authority (KİK) decision text in paginated Markdown format",
annotations={
"readOnlyHint": True,
"idempotentHint": True
@@ -1526,7 +1721,7 @@ async def get_kik_document_markdown(
is_paginated=False
)
@app.tool(
description="Search Competition Authority (Rekabet Kurumu) decisions for competition law and antitrust research. Before using, read docs://tools/rekabet",
description="Search Competition Authority (Rekabet Kurumu) decisions for competition law and antitrust",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -1534,10 +1729,10 @@ async def get_kik_document_markdown(
}
)
async def search_rekabet_kurumu_decisions(
sayfaAdi: Optional[str] = Field(None, description="Search in decision title (Başlık)."),
YayinlanmaTarihi: Optional[str] = Field(None, description="Publication date (Yayım Tarihi), e.g., DD.MM.YYYY."),
PdfText: Optional[str] = Field(
None,
sayfaAdi: str = Field("", description="Search in decision title (Başlık)."),
YayinlanmaTarihi: str = Field("", description="Publication date (Yayım Tarihi), e.g., DD.MM.YYYY."),
PdfText: str = Field(
"",
description='Search in decision text. Use "\\"kesin cümle\\"" for precise matching.'
),
KararTuru: Literal[
@@ -1548,8 +1743,8 @@ async def search_rekabet_kurumu_decisions(
"Özelleştirme",
"Rekabet İhlali"
] = Field("ALL", description="Parameter description"),
KararSayisi: Optional[str] = Field(None, description="Decision number (Karar Sayısı)."),
KararTarihi: Optional[str] = Field(None, description="Decision date (Karar Tarihi), e.g., DD.MM.YYYY."),
KararSayisi: str = Field("", description="Decision number (Karar Sayısı)."),
KararTarihi: str = Field("", description="Decision date (Karar Tarihi), e.g., DD.MM.YYYY."),
page: int = Field(1, ge=1, description="Page number to fetch for the results list.")
) -> RekabetSearchResult:
"""Search Competition Authority decisions."""
@@ -1582,7 +1777,7 @@ async def search_rekabet_kurumu_decisions(
return RekabetSearchResult(decisions=[], retrieved_page_number=page, total_records_found=0, total_pages=0)
@app.tool(
description="Retrieve full text of a Competition Authority decision in paginated Markdown format. Before using, read docs://tools/rekabet",
description="Get Competition Authority decision text in paginated Markdown format",
annotations={
"readOnlyHint": True,
"idempotentHint": True
@@ -1606,7 +1801,7 @@ async def get_rekabet_kurumu_document(
# --- MCP Tools for Bedesten (Unified Search Across All Courts) ---
@app.tool(
description="Search Yargıtay, Danıştay, Local Courts, Appeals Courts, and KYB decisions",
description="Search multiple Turkish courts (Yargıtay, Danıştay, Local Courts, Appeals Courts, KYB)",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -1614,7 +1809,7 @@ async def get_rekabet_kurumu_document(
}
)
async def search_bedesten_unified(
phrase: str = Field(..., description="Search phrase. Use \"exact phrase\" for precise matching"),
phrase: str = Field(..., description="Turkish search phrase. Supports +required -excluded \"exact phrase\" operators"),
court_types: List[BedestenCourtTypeEnum] = Field(
default=["YARGITAYKARARI", "DANISTAYKARAR"],
description="Court types: YARGITAYKARARI, DANISTAYKARAR, YERELHUKUK, ISTINAFHUKUK, KYB"
@@ -1622,8 +1817,8 @@ async def search_bedesten_unified(
pageSize: int = Field(10, ge=1, le=10, description="Results per page (1-10)"),
pageNumber: int = Field(1, ge=1, description="Page number"),
birimAdi: Optional[Union[YargitayBirimEnum, DanistayBirimEnum]] = Field(None, description="Chamber filter (optional)"),
kararTarihiStart: Optional[str] = Field(None, description="Start date (ISO 8601 format)"),
kararTarihiEnd: Optional[str] = Field(None, description="End date (ISO 8601 format)")
kararTarihiStart: str = Field("", description="Start date (ISO 8601 format)"),
kararTarihiEnd: str = Field("", description="End date (ISO 8601 format)")
) -> dict:
"""Search Turkish legal databases via unified Bedesten API."""
search_data = BedestenSearchData(
@@ -1665,7 +1860,7 @@ async def search_bedesten_unified(
raise
@app.tool(
description="Retrieve legal decision document from Bedesten API in Markdown format",
description="Get legal decision document from Bedesten API in Markdown format",
annotations={
"readOnlyHint": True,
"idempotentHint": True
@@ -1689,7 +1884,7 @@ async def get_bedesten_document_markdown(
# --- MCP Tools for Sayıştay (Turkish Court of Accounts) ---
@app.tool(
description="Search Sayıştay Genel Kurul decisions for audit and accountability regulations. Before using, read docs://tools/sayistay",
description="Search Sayıştay Genel Kurul decisions for audit and accountability regulations",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -1697,11 +1892,11 @@ async def get_bedesten_document_markdown(
}
)
async def search_sayistay_genel_kurul(
karar_no: Optional[str] = Field(None, description="Decision number to search for (e.g., '5415')"),
karar_ek: Optional[str] = Field(None, description="Decision appendix number (max 99, e.g., '1')"),
karar_tarih_baslangic: Optional[str] = Field(None, description="See docs for details"),
karar_tarih_bitis: Optional[str] = Field(None, description="See docs for details"),
karar_tamami: Optional[str] = Field(None, description="See docs for details"),
karar_no: str = Field("", description="Decision number to search for (e.g., '5415')"),
karar_ek: str = Field("", description="Decision appendix number (max 99, e.g., '1')"),
karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY)"),
karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY)"),
karar_tamami: str = Field("", description="Full text search"),
start: int = Field(0, description="Starting record for pagination (0-based)"),
length: int = Field(10, description="Number of records per page (1-100)")
) -> GenelKurulSearchResponse:
@@ -1724,7 +1919,7 @@ async def search_sayistay_genel_kurul(
raise
@app.tool(
description="Search Sayıştay Temyiz Kurulu decisions with chamber filtering and comprehensive search criteria. Before using, read docs://tools/sayistay",
description="Search Sayıştay Temyiz Kurulu decisions with chamber filtering and comprehensive criteria",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -1732,16 +1927,16 @@ async def search_sayistay_genel_kurul(
}
)
async def search_sayistay_temyiz_kurulu(
ilam_dairesi: DaireEnum = Field("ALL", description="See docs for details"),
yili: Optional[str] = Field(None, description="See docs for details"),
karar_tarih_baslangic: Optional[str] = Field(None, description="See docs for details"),
karar_tarih_bitis: Optional[str] = Field(None, description="See docs for details"),
kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="See docs for details"),
ilam_no: Optional[str] = Field(None, description="Audit report number (İlam No, max 50 chars)"),
dosya_no: Optional[str] = Field(None, description="File number for the case"),
temyiz_tutanak_no: Optional[str] = Field(None, description="Appeals board meeting minutes number"),
temyiz_karar: Optional[str] = Field(None, description="See docs for details"),
web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="See docs for details"),
ilam_dairesi: DaireEnum = Field("ALL", description="Audit chamber selection"),
yili: str = Field("", description="Year (YYYY)"),
karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY)"),
karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY)"),
kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Public admin type"),
ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)"),
dosya_no: str = Field("", description="File number for the case"),
temyiz_tutanak_no: str = Field("", description="Appeals board meeting minutes number"),
temyiz_karar: str = Field("", description="Appeals decision text"),
web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Decision subject"),
start: int = Field(0, description="Starting record for pagination (0-based)"),
length: int = Field(10, description="Number of records per page (1-100)")
) -> TemyizKuruluSearchResponse:
@@ -1769,7 +1964,7 @@ async def search_sayistay_temyiz_kurulu(
raise
@app.tool(
description="Search Sayıştay Daire decisions with chamber filtering and subject categorization. Before using, read docs://tools/sayistay",
description="Search Sayıştay Daire decisions with chamber filtering and subject categorization",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -1777,14 +1972,14 @@ async def search_sayistay_temyiz_kurulu(
}
)
async def search_sayistay_daire(
yargilama_dairesi: DaireEnum = Field("ALL", description="See docs for details"),
karar_tarih_baslangic: Optional[str] = Field(None, description="See docs for details"),
karar_tarih_bitis: Optional[str] = Field(None, description="See docs for details"),
ilam_no: Optional[str] = Field(None, description="Audit report number (İlam No, max 50 chars)"),
kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="See docs for details"),
hesap_yili: Optional[str] = Field(None, description="See docs for details"),
web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="See docs for details"),
web_karar_metni: Optional[str] = Field(None, description="See docs for details"),
yargilama_dairesi: DaireEnum = Field("ALL", description="Chamber selection"),
karar_tarih_baslangic: str = Field("", description="Start date (DD.MM.YYYY)"),
karar_tarih_bitis: str = Field("", description="End date (DD.MM.YYYY)"),
ilam_no: str = Field("", description="Audit report number (İlam No, max 50 chars)"),
kamu_idaresi_turu: KamuIdaresiTuruEnum = Field("ALL", description="Public admin type"),
hesap_yili: str = Field("", description="Fiscal year"),
web_karar_konusu: WebKararKonusuEnum = Field("ALL", description="Decision subject"),
web_karar_metni: str = Field("", description="Decision text search"),
start: int = Field(0, description="Starting record for pagination (0-based)"),
length: int = Field(10, description="Number of records per page (1-100)")
) -> DaireSearchResponse:
@@ -1810,7 +2005,7 @@ async def search_sayistay_daire(
raise
@app.tool(
description="Retrieve Sayıştay Genel Kurul decision document in Markdown format. Before using, read docs://tools/sayistay",
description="Get Sayıştay Genel Kurul decision document in Markdown format",
annotations={
"readOnlyHint": True,
"openWorldHint": False,
@@ -1833,7 +2028,7 @@ async def get_sayistay_genel_kurul_document_markdown(
raise
@app.tool(
description="Retrieve Sayıştay Temyiz Kurulu decision document in Markdown format. Before using, read docs://tools/sayistay",
description="Get Sayıştay Temyiz Kurulu decision document in Markdown format",
annotations={
"readOnlyHint": True,
"openWorldHint": False,
@@ -1856,7 +2051,7 @@ async def get_sayistay_temyiz_kurulu_document_markdown(
raise
@app.tool(
description="Retrieve Sayıştay Daire decision document in Markdown format. Before using, read docs://tools/sayistay",
description="Get Sayıştay Daire decision document in Markdown format",
annotations={
"readOnlyHint": True,
"openWorldHint": False,
@@ -2085,7 +2280,7 @@ async def check_government_servers_health() -> Dict[str, Any]:
# --- MCP Tools for KVKK ---
@app.tool(
description="Search KVKK decisions using Brave Search API for data protection authority decisions. Before using, read docs://tools/kvkk",
description="Search KVKK data protection authority decisions",
annotations={
"readOnlyHint": True,
"openWorldHint": True,
@@ -2093,7 +2288,7 @@ async def check_government_servers_health() -> Dict[str, Any]:
}
)
async def search_kvkk_decisions(
keywords: str = Field(..., description="Turkish keywords for KVKK decision search"),
keywords: str = Field(..., description="Turkish keywords. Supports +required -excluded \"exact phrase\" operators"),
page: int = Field(1, ge=1, le=50, description="Page number for results (1-50)."),
pageSize: int = Field(10, ge=1, le=20, description="Number of results per page (1-20).")
) -> KvkkSearchResult:
@@ -2122,7 +2317,7 @@ async def search_kvkk_decisions(
)
@app.tool(
description="Retrieve KVKK decision document in Markdown format with metadata extraction. Before using, read docs://tools/kvkk",
description="Get KVKK decision document in Markdown format with metadata extraction",
annotations={
"readOnlyHint": True,
"openWorldHint": False,
@@ -2130,7 +2325,7 @@ async def search_kvkk_decisions(
}
)
async def get_kvkk_document_markdown(
decision_url: str = Field(..., description="See docs for details"),
decision_url: str = Field(..., description="KVKK decision URL from search results"),
page_number: Union[int, str] = Field(1, description="Page number for paginated Markdown content (1-indexed, accepts int). Default is 1 (first 5,000 characters).")
) -> KvkkDocumentMarkdown:
"""Get KVKK decision as paginated Markdown."""
@@ -2499,6 +2694,8 @@ async def fetch(
logger.exception(f"Error fetching ChatGPT Deep Research document {id}")
raise
# --- Token Metrics Tool Removed for Optimization ---
def ensure_playwright_browsers():
"""Ensure Playwright browsers are installed for KIK tool functionality."""
try: