14 Commits
Author SHA1 Message Date
saidsurucu 82a0d13d25 Bump version to 0.1.8 for Gemini CLI compatibility fixes 2025-11-21 22:21:19 +03:00
saidsurucu 18b552ca2f Fix SearchResultItem reference error for Gemini CLI
- Fixed search() function to return Dict[str, Any] instead of SearchResponse
- Converted return statements to plain dictionaries
- Deleted unused Pydantic models (SearchResultItem, SearchResponse)
- Eliminates /SearchResultItem references that Gemini CLI cannot resolve
- All MCP tools now compatible with Gemini CLI schema validation
2025-11-21 22:18:36 +03:00
saidsurucu 1e96b1888e Fix Gemini CLI compatibility: Convert all Pydantic return types to Dict[str, Any]
- Fixed 10 tools to avoid / patterns in JSON schemas
- All tools now return Dict[str, Any] with .model_dump() applied
- Affected tools:
  * search_emsal_detailed_decisions
  * get_emsal_document_markdown
  * search_uyusmazlik_decisions
  * get_uyusmazlik_document_markdown_from_url
  * search_rekabet_kurumu_decisions
  * get_rekabet_kurumu_document
  * search_sayistay_unified
  * get_sayistay_document_unified
  * search_kvkk_decisions
  * get_kvkk_document_markdown
- Gemini CLI should now be able to load and use all MCP tools without schema validation errors
2025-11-21 22:13:51 +03:00
saidsurucu 815786a09d Comment out undefined LOG_FILE_PATH reference 2025-11-21 21:57:21 +03:00
saidsurucu 260adb3ac9 Fix Python 3.11 compatibility in KİK v2 client 2025-11-21 21:56:10 +03:00
saidsurucu 7164205425 Fix Gemini CLI schema error in search tool 2025-11-21 21:33:39 +03:00
saidsurucu 3961a23d3a Make tiktoken and PyJWT optional (saas group only) 2025-10-06 15:45:16 +03:00
saidsurucu 25723f070f Remove file logging - console only 2025-10-06 15:21:03 +03:00
saidsurucu 6376037ccf Add tiktoken and PyJWT to requirements.txt 2025-10-06 15:04:18 +03:00
saidsurucu d1728ce114 Fix dependency installation order 2025-10-06 15:02:34 +03:00
saidsurucu 69b5da5cef Add tiktoken to saas dependencies 2025-10-06 14:55:31 +03:00
saidsurucu 91564bf0a1 Remove logging statements from asgi_app 2025-10-06 14:49:28 +03:00
saidsurucu 6f94eca33c kik v2 update 2025-09-02 20:00:53 +03:00
saidsurucu 4122790821 Bump version to 0.1.7 - KİK v2 implementation with three decision types
- Add comprehensive KİK v2 MCP implementation
- Support for all three decision types: uyusmazlik, duzenleyici, mahkeme
- Tested with 826 total decisions across all types
- SSL legacy server support for compatibility
- Hash analysis and document ID encryption research completed
2025-09-02 19:58:40 +03:00
12 changed files with 3702 additions and 217 deletions
+1
View File
@@ -213,3 +213,4 @@ measure_mcp_directly.py
playwright_mcp_overhead.json playwright_mcp_overhead.json
simple_test.py simple_test.py
analyze_anayasa_html.py analyze_anayasa_html.py
CLAUDE.md
+1
View File
@@ -0,0 +1 @@
/cache
+84
View File
@@ -0,0 +1,84 @@
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp csharp_omnisharp
# dart elixir elm erlang fortran go
# haskell java julia kotlin lua markdown
# nix perl php python python_jedi r
# rego ruby ruby_solargraph rust scala swift
# terraform typescript typescript_vts yaml zig
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# Special requirements:
# - csharp: Requires the presence of a .sln file in the project folder.
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- python
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "yargi-mcp"
included_optional_tools: []
+53 -9
View File
@@ -72,7 +72,10 @@ uv run simple_test.py # Simple individual tool test
uv run measure_mcp_directly.py # Measure MCP token overhead uv run measure_mcp_directly.py # Measure MCP token overhead
# Test KVKK module (uses fallback token if BRAVE_API_TOKEN not set) # Test KVKK module (uses fallback token if BRAVE_API_TOKEN not set)
python test_kvkk_module.py # Test KVKK search and document retrieval python test_kvkv_module.py # Test KVKK search and document retrieval
# Test KİK v2 comprehensive functionality
uv run test_kik_v2_comprehensive.py # Test all three KİK v2 decision types (uyusmazlik, duzenleyici, mahkeme)
# MCP Optimization Testing # MCP Optimization Testing
uv run test_core_tools_quick.py # Verify all tools work after optimization uv run test_core_tools_quick.py # Verify all tools work after optimization
@@ -182,7 +185,7 @@ This MCP server has undergone comprehensive optimization to minimize token overh
4. **emsal_mcp_module**: Emsal (UYAP precedent) decisions 4. **emsal_mcp_module**: Emsal (UYAP precedent) decisions
5. **uyusmazlik_mcp_module**: Uyuşmazlık Mahkemesi (Jurisdictional Disputes Court) 5. **uyusmazlik_mcp_module**: Uyuşmazlık Mahkemesi (Jurisdictional Disputes Court)
6. **anayasa_mcp_module**: Constitutional Court (both norm control and individual applications) 6. **anayasa_mcp_module**: Constitutional Court (both norm control and individual applications)
7. **kik_mcp_module**: KİK (Public Procurement Authority) decisions 7. **kik_mcp_module**: KİK (Public Procurement Authority) decisions with v2 API support for three decision types (uyusmazlik, duzenleyici, mahkeme)
8. **rekabet_mcp_module**: Competition Authority decisions 8. **rekabet_mcp_module**: Competition Authority decisions
9. **kvkk_mcp_module**: KVKK (Personal Data Protection Authority) decisions - Brave API integration 9. **kvkk_mcp_module**: KVKK (Personal Data Protection Authority) decisions - Brave API integration
10. **bddk_mcp_module**: BDDK (Banking Regulation and Supervision Agency) decisions - Tavily API integration 10. **bddk_mcp_module**: BDDK (Banking Regulation and Supervision Agency) decisions - Tavily API integration
@@ -1077,15 +1080,21 @@ yargi-mcp
### Date Filtering Format (Bedesten API) ### Date Filtering Format (Bedesten API)
All Bedesten API tools support consistent date filtering: All Bedesten API tools support consistent date filtering with **automatic format conversion**:
- **Format**: ISO 8601 with Z timezone: `YYYY-MM-DDTHH:MM:SS.000Z` - **Accepted Formats**:
- Simple format: `YYYY-MM-DD` (e.g., `2020-01-01`) - **automatically converted**
- Full ISO 8601: `YYYY-MM-DDTHH:MM:SS.000Z` (e.g., `2020-01-01T00:00:00.000Z`)
- **Automatic Conversion**: ✅ **NEW FEATURE** - Simple dates are automatically converted to ISO 8601 format
- Start dates: `2020-01-01` → `2020-01-01T00:00:00.000Z`
- End dates: `2020-01-01` → `2020-01-01T23:59:59.999Z`
- **Parameters**: `kararTarihiStart` (start date) and `kararTarihiEnd` (end date) - **Parameters**: `kararTarihiStart` (start date) and `kararTarihiEnd` (end date)
- **Usage**: Both parameters are optional, use together for date ranges or single parameter for one-sided filtering - **Usage**: Both parameters are optional, use together for date ranges or single parameter for one-sided filtering
- **Examples**: - **Examples**:
- Single date: `kararTarihiStart="2024-06-25T00:00:00.000Z", kararTarihiEnd="2024-06-25T23:59:59.999Z"` - **Simple format**: `kararTarihiStart="2024-06-25", kararTarihiEnd="2024-06-25"` ✅ **Works automatically**
- Year range: `kararTarihiStart="2024-01-01T00:00:00.000Z", kararTarihiEnd="2024-12-31T23:59:59.999Z"` - **Year range**: `kararTarihiStart="2024-01-01", kararTarihiEnd="2024-12-31"` ✅ **Auto-converted**
- From date: `kararTarihiStart="2024-01-01T00:00:00.000Z"` (no end date) - **Full ISO format**: `kararTarihiStart="2024-01-01T00:00:00.000Z", kararTarihiEnd="2024-12-31T23:59:59.999Z"`
- Until date: `kararTarihiEnd="2024-12-31T23:59:59.999Z"` (no start date) - **From date**: `kararTarihiStart="2024-01-01"` (no end date)
- **Until date**: `kararTarihiEnd="2024-12-31"` (no start date)
### Exact Phrase Search Format (Bedesten API) ### Exact Phrase Search Format (Bedesten API)
All Bedesten API tools support two types of phrase searching: All Bedesten API tools support two types of phrase searching:
@@ -2054,7 +2063,7 @@ build-backend = "setuptools.build_meta"
4. **Emsal**: 2 tools (search + document) 4. **Emsal**: 2 tools (search + document)
5. **Uyuşmazlık**: 2 tools (search + document) 5. **Uyuşmazlık**: 2 tools (search + document)
6. **Constitutional Court**: ✅ 2 tools (unified norm control + individual applications) - **NEWLY UNIFIED** 6. **Constitutional Court**: ✅ 2 tools (unified norm control + individual applications) - **NEWLY UNIFIED**
7. **KİK**: 2 tools (search + document) 7. **KİK**: 2 tools (search + document) - **v2 API with three decision types: uyusmazlik, duzenleyici, mahkeme** ✅
8. **Competition Authority**: 2 tools (search + document) 8. **Competition Authority**: 2 tools (search + document)
9. **KVKK**: 2 tools (search + document) 9. **KVKK**: 2 tools (search + document)
10. **Sayıştay**: 4 tools (3 search types + document) 10. **Sayıştay**: 4 tools (3 search types + document)
@@ -2103,6 +2112,41 @@ build-backend = "setuptools.build_meta"
- **Result**: Bedesten tools now handle API edge cases gracefully without crashing - **Result**: Bedesten tools now handle API edge cases gracefully without crashing
- **Production Status**: Deployed and operational on api.yargimcp.com - **Production Status**: Deployed and operational on api.yargimcp.com
#### ✅ Automatic Date Format Conversion (Completed - Sep 2, 2025)
- **Issue**: Bedesten API requires ISO 8601 format with timezone, but users were providing simple dates
- **Problem**: Queries like `kararTarihiStart="2020-01-01"` returned "No data returned from Bedesten API"
- **Root Cause**: Simple date format `YYYY-MM-DD` not converted to required `YYYY-MM-DDTHH:MM:SS.000Z` format
- **Solution Applied**: ✅ **Automatic Date Format Conversion** in `search_bedesten_unified`
- **Start dates**: `2020-01-01` → `2020-01-01T00:00:00.000Z` (beginning of day)
- **End dates**: `2020-01-01` → `2020-01-01T23:59:59.999Z` (end of day)
- **Backwards compatible**: Full ISO 8601 dates still work unchanged
- **Smart detection**: Only converts if date doesn't already end with 'Z'
- **Benefits**:
- ✅ **User-friendly**: Simple date input now works seamlessly
- ✅ **Inclusive ranges**: End dates include the entire specified day
- ✅ **No breaking changes**: Existing ISO 8601 usage unaffected
- **Production Status**: Deployed on api.yargimcp.com - **Version 353**
#### ✅ KİK v2 MCP Implementation Testing (Completed - Sep 2, 2025)
- **Issue**: Test KİK v2 MCP implementation with all three decision types
- **Request**: "üç karar türü ile de mcpyi test et" (test the MCP with all three decision types)
- **Decision Types Tested**:
- ✅ **uyusmazlik** (dispute) - 500 decisions found and searchable
- ✅ **duzenleyici** (regulatory) - 8 decisions found and searchable
- ✅ **mahkeme** (court) - 318 decisions found and searchable
- **Total Coverage**: 826 decisions across all three decision types
- **SSL Issues**: ✅ Resolved with legacy server connect configuration
- **API Endpoints**: All three endpoints working correctly
- `/api/KurulKararlari/GetKurulKararlari` (uyusmazlik)
- `/api/KurulKararlari/GetKurulKararlariDk` (duzenleyici)
- `/api/KurulKararlari/GetKurulKararlariMk` (mahkeme)
- **Hash Analysis**: Comprehensive testing performed to understand document ID encryption
- Tested various hash generation patterns (SHA256, HMAC, composite hashes)
- Angular/cryptoService.encrypt() style approaches tested
- Hash eşleşmesi bulunamadı - client-side session data veya farklı algoritma kullanılıyor olabilir
- **Result**: ✅ KİK v2 MCP implementation fully operational for all three decision types
- **Production Status**: All tests passing, search functionality working, ready for production deployment
### Key Features ### Key Features
- **FastMCP Framework**: Modern MCP server implementation - **FastMCP Framework**: Modern MCP server implementation
- **Unified APIs**: Single interface for multiple court systems - **Unified APIs**: Single interface for multiple court systems
+2 -1
View File
@@ -9,10 +9,11 @@ COPY pyproject.toml poetry.lock* requirements*.txt* ./
# Fast, deterministic install with `uv` # Fast, deterministic install with `uv`
RUN pip install --no-cache-dir uv && \ RUN pip install --no-cache-dir uv && \
uv pip install --system --no-cache-dir . && \
uv pip install --system --no-cache-dir .[asgi,saas] uv pip install --system --no-cache-dir .[asgi,saas]
# Cache buster - force rebuild # Cache buster - force rebuild
ARG CACHE_BUST=202507221015 ARG CACHE_BUST=202510061202
RUN echo "Cache bust: $CACHE_BUST" RUN echo "Cache bust: $CACHE_BUST"
# Copy application source # Copy application source
+239
View File
@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""
Analyze KİK v2 hash generation by examining JavaScript code patterns
and trying to reverse engineer the hash generation logic.
"""
import asyncio
import json
import hashlib
import hmac
import base64
from fastmcp import Client
from mcp_server_main import app
def analyze_webpack_hash_patterns():
"""
Analyze the webpack JavaScript code you provided to find hash generation patterns
"""
print("🔍 Analyzing webpack hash generation patterns...")
# From the JavaScript code, I can see several hash/ID generation patterns:
hash_patterns = {
# Webpack chunk system hashes (from the JS code)
"webpack_chunks": {
315: "d9a9486a4f5ba326",
531: "cd8fb385c88033ae",
671: "04c48b287646627a",
856: "682c9a7b87351f90",
1017: "9de022378fc275f6",
# ... many more from the __webpack_require__.u function
},
# Symbol generation from Zone.js
"zone_symbols": [
"__zone_symbol__",
"__Zone_symbol_prefix",
"Zone.__symbol__"
],
# Angular module federation patterns
"module_federation": [
"__webpack_modules__",
"__webpack_module_cache__",
"__webpack_require__"
]
}
# The target hash format
target_hash = "42f9bcd59e0dfbca36dec9accf5686c7a92aa97724cd8fc3550beb84b80409da"
print(f"🎯 Target hash: {target_hash}")
print(f" Length: {len(target_hash)} characters")
print(f" Format: {'SHA256' if len(target_hash) == 64 else 'Other'} (64 chars = SHA256)")
return hash_patterns
def test_webpack_style_hashing(data_dict):
"""Test webpack-style hash generation methods"""
hashes = {}
for key, value in data_dict.items():
test_string = str(value)
# Try various webpack-style hash methods
hashes[f"webpack_md5_{key}"] = hashlib.md5(test_string.encode()).hexdigest()
hashes[f"webpack_sha1_{key}"] = hashlib.sha1(test_string.encode()).hexdigest()
hashes[f"webpack_sha256_{key}"] = hashlib.sha256(test_string.encode()).hexdigest()
# Try with various prefixes/suffixes (common in webpack)
prefixed = f"__webpack__{test_string}"
hashes[f"webpack_prefixed_sha256_{key}"] = hashlib.sha256(prefixed.encode()).hexdigest()
# Try with module federation style
module_style = f"shell:{test_string}"
hashes[f"module_fed_sha256_{key}"] = hashlib.sha256(module_style.encode()).hexdigest()
# Try JSON stringified
json_style = json.dumps({"id": value, "type": "decision"}, separators=(',', ':'))
hashes[f"json_sha256_{key}"] = hashlib.sha256(json_style.encode()).hexdigest()
# Try with timestamp or sequence
with_seq = f"{test_string}_0"
hashes[f"seq_sha256_{key}"] = hashlib.sha256(with_seq.encode()).hexdigest()
return hashes
def test_angular_routing_hashes(data_dict):
"""Test Angular routing/state management hash generation"""
hashes = {}
for key, value in data_dict.items():
# Angular often uses route parameters for hash generation
route_style = f"/kurul-kararlari/{value}"
hashes[f"route_sha256_{key}"] = hashlib.sha256(route_style.encode()).hexdigest()
# Component state style
state_style = f"KurulKararGoster_{value}"
hashes[f"state_sha256_{key}"] = hashlib.sha256(state_style.encode()).hexdigest()
# Angular module style
module_style = f"kik.kurul.karar.{value}"
hashes[f"module_sha256_{key}"] = hashlib.sha256(module_style.encode()).hexdigest()
return hashes
def test_base64_encoding_variants(data_dict):
"""Test various base64 and encoding variants"""
hashes = {}
for key, value in data_dict.items():
test_string = str(value)
# Try base64 encoding then hashing
b64_encoded = base64.b64encode(test_string.encode()).decode()
hashes[f"b64_sha256_{key}"] = hashlib.sha256(b64_encoded.encode()).hexdigest()
# Try URL-safe base64
b64_url = base64.urlsafe_b64encode(test_string.encode()).decode()
hashes[f"b64url_sha256_{key}"] = hashlib.sha256(b64_url.encode()).hexdigest()
# Try hex encoding
hex_encoded = test_string.encode().hex()
hashes[f"hex_sha256_{key}"] = hashlib.sha256(hex_encoded.encode()).hexdigest()
return hashes
async def test_hash_generation_comprehensive():
print("🔐 Comprehensive KİK document hash generation analysis...")
print("=" * 70)
# First analyze the webpack patterns
webpack_patterns = analyze_webpack_hash_patterns()
client = Client(app)
async with client:
print("✅ MCP client connected")
# Get sample decisions
print("\n📊 Getting sample decisions for hash analysis...")
search_result = await client.call_tool("search_kik_v2_decisions", {
"decision_type": "uyusmazlik",
"karar_metni": "2024"
})
if hasattr(search_result, 'content') and search_result.content:
search_data = json.loads(search_result.content[0].text)
decisions = search_data.get('decisions', [])
if decisions:
print(f"✅ Found {len(decisions)} decisions")
# Test with first decision
sample_decision = decisions[0]
print(f"\n📋 Sample decision for hash analysis:")
for key, value in sample_decision.items():
print(f" {key}: {value}")
target_hash = "42f9bcd59e0dfbca36dec9accf5686c7a92aa97724cd8fc3550beb84b80409da"
print(f"\n🎯 Target hash to match: {target_hash}")
all_hashes = {}
# Test different hash generation methods
print(f"\n🔨 Testing webpack-style hashing...")
webpack_hashes = test_webpack_style_hashing(sample_decision)
all_hashes.update(webpack_hashes)
print(f"🔨 Testing Angular routing hashes...")
angular_hashes = test_angular_routing_hashes(sample_decision)
all_hashes.update(angular_hashes)
print(f"🔨 Testing base64 encoding variants...")
b64_hashes = test_base64_encoding_variants(sample_decision)
all_hashes.update(b64_hashes)
# Check for matches
print(f"\n🎯 Checking for hash matches...")
matches_found = []
partial_matches = []
for hash_name, hash_value in all_hashes.items():
if hash_value == target_hash:
matches_found.append((hash_name, hash_value))
print(f" 🎉 EXACT MATCH FOUND: {hash_name}")
elif hash_value[:8] == target_hash[:8]: # First 8 chars match
partial_matches.append((hash_name, hash_value))
print(f" 🔍 Partial match (first 8): {hash_name} -> {hash_value[:16]}...")
elif hash_value[-8:] == target_hash[-8:]: # Last 8 chars match
partial_matches.append((hash_name, hash_value))
print(f" 🔍 Partial match (last 8): {hash_name} -> ...{hash_value[-16:]}")
if not matches_found and not partial_matches:
print(f" ❌ No matches found")
print(f"\n📝 Sample generated hashes (first 10):")
for i, (hash_name, hash_value) in enumerate(list(all_hashes.items())[:10]):
print(f" {hash_name}: {hash_value}")
# Try combinations with other decisions
print(f"\n🔄 Testing hash combinations with multiple decisions...")
if len(decisions) > 1:
for i, decision in enumerate(decisions[1:3]): # Test 2 more
print(f"\n Testing decision {i+2}: {decision.get('kararNo')}")
decision_hashes = test_webpack_style_hashing(decision)
for hash_name, hash_value in decision_hashes.items():
if hash_value == target_hash:
print(f" 🎉 MATCH FOUND in decision {i+2}: {hash_name}")
matches_found.append((f"decision_{i+2}_{hash_name}", hash_value))
# Try composite hashes (combining multiple fields)
print(f"\n🔗 Testing composite hash generation...")
composite_tests = [
f"{sample_decision.get('gundemMaddesiId')}_{sample_decision.get('kararNo')}",
f"{sample_decision.get('kararNo')}_{sample_decision.get('kararTarihi')}",
f"uyusmazlik_{sample_decision.get('gundemMaddesiId')}_{sample_decision.get('kararTarihi')}",
json.dumps(sample_decision, separators=(',', ':'), sort_keys=True),
f"{sample_decision.get('basvuran')}_{sample_decision.get('gundemMaddesiId')}",
]
for i, composite_str in enumerate(composite_tests):
composite_hash = hashlib.sha256(composite_str.encode()).hexdigest()
if composite_hash == target_hash:
print(f" 🎉 COMPOSITE MATCH FOUND: test_{i} -> {composite_str[:50]}...")
matches_found.append((f"composite_{i}", composite_hash))
print(f"\n🎯 Hash analysis completed!")
print(f" Total matches found: {len(matches_found)}")
print(f" Partial matches: {len(partial_matches)}")
else:
print("❌ No decisions found")
else:
print("❌ Search failed")
print("=" * 70)
if __name__ == "__main__":
asyncio.run(test_hash_generation_comprehensive())
+28 -55
View File
@@ -24,11 +24,24 @@ from starlette.middleware.base import BaseHTTPMiddleware
# Import the proper create_app function that includes all middleware # Import the proper create_app function that includes all middleware
from mcp_server_main import create_app from mcp_server_main import create_app
# Import Stripe webhook router # Conditional auth-related imports (only if auth enabled)
from stripe_webhook import router as stripe_router _auth_check = os.getenv("ENABLE_AUTH", "false").lower() == "true"
# Import simplified MCP Auth HTTP adapter if _auth_check:
from mcp_auth_http_simple import router as mcp_auth_router # Import MCP Auth HTTP adapter (OAuth endpoints)
try:
from mcp_auth_http_simple import router as mcp_auth_router
except ImportError:
mcp_auth_router = None
# Import Stripe webhook router
try:
from stripe_webhook import router as stripe_router
except ImportError:
stripe_router = None
else:
mcp_auth_router = None
stripe_router = None
# OAuth configuration from environment variables # OAuth configuration from environment variables
CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://clerk.yargimcp.com") CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://clerk.yargimcp.com")
@@ -62,15 +75,13 @@ if CLERK_SECRET_KEY and CLERK_ISSUER:
# Production: Use Clerk JWKS endpoint for token validation # Production: Use Clerk JWKS endpoint for token validation
bearer_auth = BearerAuthProvider( bearer_auth = BearerAuthProvider(
jwks_uri=f"{CLERK_ISSUER}/.well-known/jwks.json", jwks_uri=f"{CLERK_ISSUER}/.well-known/jwks.json",
issuer=None, # Disable issuer validation - Clerk uses different issuer format issuer=None,
algorithm="RS256", algorithm="RS256",
audience=None, # Disable audience validation - Clerk uses different audience format audience=None,
required_scopes=[] # Disable scope validation - Clerk JWT has ['read', 'search'] required_scopes=[]
) )
logger.info(f"Bearer auth configured with Clerk JWKS: {CLERK_ISSUER}/.well-known/jwks.json")
else: else:
# Development: Generate RSA key pair for testing # Development: Generate RSA key pair for testing
logger.warning("No Clerk credentials found - using development RSA key pair")
dev_key_pair = RSAKeyPair.generate() dev_key_pair = RSAKeyPair.generate()
bearer_auth = BearerAuthProvider( bearer_auth = BearerAuthProvider(
public_key=dev_key_pair.public_key, public_key=dev_key_pair.public_key,
@@ -78,31 +89,13 @@ else:
audience="dev-mcp-server", audience="dev-mcp-server",
required_scopes=["yargi.read"] required_scopes=["yargi.read"]
) )
# Generate a test token for development
dev_token = dev_key_pair.create_token(
subject="dev-user",
issuer="https://dev.yargimcp.com",
audience="dev-mcp-server",
scopes=["yargi.read", "yargi.search"],
expires_in_seconds=3600 * 24 # 24 hours for development
)
logger.info(f"Development Bearer token: {dev_token}")
# Create MCP app with Bearer authentication # Create MCP app with Bearer authentication
mcp_server = create_app(auth=bearer_auth if auth_enabled else None) mcp_server = create_app(auth=bearer_auth if auth_enabled else None)
# Create MCP Starlette sub-application with root path - mount will add /mcp prefix # Create MCP Starlette sub-application with root path - mount will add /mcp prefix
mcp_app = mcp_server.http_app(path="/") mcp_app = mcp_server.http_app(path="/")
logger.info(f"MCP Starlette app created - type: {type(mcp_app)}, has routes: {hasattr(mcp_app, 'routes')}")
# Debug FastMCP routes
if hasattr(mcp_app, 'routes'):
logger.info(f"MCP app route count: {len(mcp_app.routes)}")
for i, route in enumerate(mcp_app.routes):
logger.info(f"Route {i}: {route.path if hasattr(route, 'path') else 'unknown'} - {type(route)}")
else:
logger.warning("MCP app has no routes attribute")
# Configure JSON encoder for proper Turkish character support # Configure JSON encoder for proper Turkish character support
class UTF8JSONResponse(JSONResponse): class UTF8JSONResponse(JSONResponse):
@@ -141,11 +134,12 @@ app = FastAPI(
redirect_slashes=False # Disable to prevent 307 redirects on /mcp endpoint redirect_slashes=False # Disable to prevent 307 redirects on /mcp endpoint
) )
# Add Stripe webhook router to FastAPI # Add auth-related routers to FastAPI (only if available)
app.include_router(stripe_router, prefix="/api/stripe") if stripe_router:
app.include_router(stripe_router, prefix="/api/stripe")
# Add MCP Auth HTTP adapter to FastAPI (handles OAuth endpoints) if mcp_auth_router:
app.include_router(mcp_auth_router) app.include_router(mcp_auth_router)
# Custom 401 exception handler for MCP spec compliance # Custom 401 exception handler for MCP spec compliance
@app.exception_handler(401) @app.exception_handler(401)
@@ -257,7 +251,6 @@ async def clerk_cors_proxy(request: Request, path: str):
) )
except Exception as e: except Exception as e:
logger.error(f"Clerk proxy error: {e}")
return JSONResponse( return JSONResponse(
{"error": "proxy_error", "message": str(e)}, {"error": "proxy_error", "message": str(e)},
status_code=500, status_code=500,
@@ -451,7 +444,6 @@ async def status():
# Simplified OAuth session validation for callback endpoints only # Simplified OAuth session validation for callback endpoints only
async def validate_clerk_session_for_oauth(request: Request, clerk_token: str = None) -> str: async def validate_clerk_session_for_oauth(request: Request, clerk_token: str = None) -> str:
"""Validate Clerk session for OAuth callback endpoints only (not for MCP endpoints)""" """Validate Clerk session for OAuth callback endpoints only (not for MCP endpoints)"""
logger.info(f"OAuth callback session validation - token provided: {bool(clerk_token)}")
try: try:
# Use Clerk SDK if available # Use Clerk SDK if available
@@ -461,48 +453,33 @@ async def validate_clerk_session_for_oauth(request: Request, clerk_token: str =
# Try JWT token first (from URL parameter) # Try JWT token first (from URL parameter)
if clerk_token: if clerk_token:
logger.info("Validating Clerk JWT token for OAuth callback")
try: try:
# Trust OAuth flow redirect - FastMCP handles full JWT validation for MCP endpoints
logger.info("OAuth JWT token accepted for callback")
return "oauth_user_from_token" return "oauth_user_from_token"
except Exception as e: except Exception as e:
logger.error(f"OAuth JWT token validation failed: {str(e)}") pass
# Fall through to cookie validation
# Fallback to cookie validation # Fallback to cookie validation
logger.info("Attempting cookie-based session validation for OAuth")
clerk_session = request.cookies.get("__session") clerk_session = request.cookies.get("__session")
if not clerk_session: if not clerk_session:
logger.error("No Clerk session cookie found")
raise HTTPException(status_code=401, detail="No Clerk session found") raise HTTPException(status_code=401, detail="No Clerk session found")
# Validate session with Clerk # Validate session with Clerk
session = clerk.sessions.verify_session(clerk_session) session = clerk.sessions.verify_session(clerk_session)
logger.info(f"OAuth cookie session validation successful - user_id: {session.user_id}")
return session.user_id return session.user_id
except ImportError: except ImportError:
# Fallback for development without Clerk SDK
logger.warning("Clerk SDK not available - using development fallback for OAuth")
return "dev_user_123" return "dev_user_123"
except Exception as e: except Exception as e:
logger.error(f"OAuth session validation failed: {str(e)}")
raise HTTPException(status_code=401, detail=f"OAuth session validation failed: {str(e)}") raise HTTPException(status_code=401, detail=f"OAuth session validation failed: {str(e)}")
# MCP OAuth Callback Endpoint # MCP OAuth Callback Endpoint
@app.get("/auth/mcp-callback") @app.get("/auth/mcp-callback")
async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)): async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
"""Handle OAuth callback for MCP token generation""" """Handle OAuth callback for MCP token generation"""
logger.info(f"MCP OAuth callback - clerk_token provided: {bool(clerk_token)}")
try: try:
# Validate Clerk session with JWT token support # Validate Clerk session with JWT token support
user_id = await validate_clerk_session_for_oauth(request, clerk_token) user_id = await validate_clerk_session_for_oauth(request, clerk_token)
logger.info(f"User authenticated successfully - user_id: {user_id}")
# Use the Clerk JWT token directly (no need to generate custom token)
logger.info("User authenticated successfully via Clerk")
# Return success response # Return success response
return HTMLResponse(f""" return HTMLResponse(f"""
@@ -538,7 +515,6 @@ async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
""") """)
except HTTPException as e: except HTTPException as e:
logger.error(f"MCP OAuth callback failed: {e.detail}")
return HTMLResponse(f""" return HTMLResponse(f"""
<html> <html>
<head> <head>
@@ -564,7 +540,6 @@ async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
</html> </html>
""", status_code=e.status_code) """, status_code=e.status_code)
except Exception as e: except Exception as e:
logger.error(f"Unexpected error in MCP OAuth callback: {str(e)}")
return HTMLResponse(f""" return HTMLResponse(f"""
<html> <html>
<head> <head>
@@ -610,7 +585,5 @@ app.mount("/mcp/", mcp_app)
# Set the lifespan context after mounting # Set the lifespan context after mounting
app.router.lifespan_context = mcp_app.lifespan app.router.lifespan_context = mcp_app.lifespan
logger.info("MCP app mounted successfully at /mcp/")
# Export for uvicorn # Export for uvicorn
__all__ = ["app"] __all__ = ["app"]
+452
View File
@@ -0,0 +1,452 @@
# kik_mcp_module/client_v2.py
import httpx
import requests
import logging
import uuid
import base64
import ssl
from typing import Optional
from datetime import datetime
from .models_v2 import (
KikV2DecisionType, KikV2SearchPayload, KikV2SearchPayloadDk, KikV2SearchPayloadMk,
KikV2RequestData, KikV2QueryRequest, KikV2KeyValuePair,
KikV2SearchResponse, KikV2SearchResponseDk, KikV2SearchResponseMk,
KikV2SearchResult, KikV2CompactDecision, KikV2DocumentMarkdown
)
logger = logging.getLogger(__name__)
class KikV2ApiClient:
"""
New KIK v2 API Client for https://ekapv2.kik.gov.tr
This client uses the modern JSON-based API endpoint that provides
better structured data compared to the legacy form-based API.
"""
BASE_URL = "https://ekapv2.kik.gov.tr"
# Endpoint mappings for different decision types
ENDPOINTS = {
KikV2DecisionType.UYUSMAZLIK: "/b_ihalearaclari/api/KurulKararlari/GetKurulKararlari",
KikV2DecisionType.DUZENLEYICI: "/b_ihalearaclari/api/KurulKararlari/GetKurulKararlariDk",
KikV2DecisionType.MAHKEME: "/b_ihalearaclari/api/KurulKararlari/GetKurulKararlariMk"
}
def __init__(self, request_timeout: float = 60.0):
# Create SSL context with legacy server support
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
# Enable legacy server connect option for older SSL implementations (Python 3.12+)
if hasattr(ssl, 'OP_LEGACY_SERVER_CONNECT'):
ssl_context.options |= ssl.OP_LEGACY_SERVER_CONNECT
# Set broader cipher suite support including legacy ciphers
ssl_context.set_ciphers('ALL:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA')
self.http_client = httpx.AsyncClient(
base_url=self.BASE_URL,
verify=ssl_context,
headers={
"Accept": "application/json",
"Accept-Language": "tr",
"Content-Type": "application/json",
"Origin": self.BASE_URL,
"Referer": f"{self.BASE_URL}/sorgulamalar/kurul-kararlari",
"Sec-Fetch-Dest": "empty",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Site": "same-origin",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"api-version": "v1",
"sec-ch-ua": '"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"'
},
timeout=request_timeout
)
# Generate security headers (these might need to be updated based on API requirements)
self.security_headers = self._generate_security_headers()
def _generate_security_headers(self) -> dict:
"""
Generate the custom security headers required by KIK v2 API.
These headers appear to be for request validation/encryption.
"""
# Generate a random GUID for each session
request_guid = str(uuid.uuid4())
# These are example values - in a real implementation, these might need
# to be calculated based on the request content or session
return {
"X-Custom-Request-Guid": request_guid,
"X-Custom-Request-R8id": "hwnOjsN8qdgtDw70x3sKkxab0rj2bQ8Uph4+C+oU+9AMmQqRN3eMOEEeet748DOf",
"X-Custom-Request-Siv": "p2IQRTitF8z7I39nBjdAqA==",
"X-Custom-Request-Ts": "1vB3Wwrt8YQ5U6t3XAzZ+Q=="
}
def _build_search_payload(self,
decision_type: KikV2DecisionType,
karar_metni: str = "",
karar_no: str = "",
basvuran: str = "",
idare_adi: str = "",
baslangic_tarihi: str = "",
bitis_tarihi: str = ""):
"""Build the search payload for KIK v2 API."""
key_value_pairs = []
# Add non-empty search criteria
if karar_metni:
key_value_pairs.append(KikV2KeyValuePair(key="KararMetni", value=karar_metni))
if karar_no:
key_value_pairs.append(KikV2KeyValuePair(key="KararNo", value=karar_no))
if basvuran:
key_value_pairs.append(KikV2KeyValuePair(key="BasvuranAdi", value=basvuran))
if idare_adi:
key_value_pairs.append(KikV2KeyValuePair(key="IdareAdi", value=idare_adi))
if baslangic_tarihi:
key_value_pairs.append(KikV2KeyValuePair(key="BaslangicTarihi", value=baslangic_tarihi))
if bitis_tarihi:
key_value_pairs.append(KikV2KeyValuePair(key="BitisTarihi", value=bitis_tarihi))
# If no search criteria provided, use a generic search
if not key_value_pairs:
key_value_pairs.append(KikV2KeyValuePair(key="KararMetni", value=""))
query_request = KikV2QueryRequest(keyValueOfstringanyType=key_value_pairs)
request_data = KikV2RequestData(keyValuePairs=query_request)
# Return appropriate payload based on decision type
if decision_type == KikV2DecisionType.UYUSMAZLIK:
return KikV2SearchPayload(sorgulaKurulKararlari=request_data)
elif decision_type == KikV2DecisionType.DUZENLEYICI:
return KikV2SearchPayloadDk(sorgulaKurulKararlariDk=request_data)
elif decision_type == KikV2DecisionType.MAHKEME:
return KikV2SearchPayloadMk(sorgulaKurulKararlariMk=request_data)
else:
raise ValueError(f"Unsupported decision type: {decision_type}")
async def search_decisions(self,
decision_type: KikV2DecisionType = KikV2DecisionType.UYUSMAZLIK,
karar_metni: str = "",
karar_no: str = "",
basvuran: str = "",
idare_adi: str = "",
baslangic_tarihi: str = "",
bitis_tarihi: str = "") -> KikV2SearchResult:
"""
Search KIK decisions using the v2 API.
Args:
decision_type: Type of decision to search (uyusmazlik/duzenleyici/mahkeme)
karar_metni: Decision text search
karar_no: Decision number (e.g., "2025/UH.II-1801")
basvuran: Applicant name
idare_adi: Administration name
baslangic_tarihi: Start date (YYYY-MM-DD format)
bitis_tarihi: End date (YYYY-MM-DD format)
Returns:
KikV2SearchResult with compact decision list
"""
logger.info(f"KikV2ApiClient: Searching {decision_type.value} decisions with criteria - karar_metni: '{karar_metni}', karar_no: '{karar_no}', basvuran: '{basvuran}'")
try:
# Build request payload
payload = self._build_search_payload(
decision_type=decision_type,
karar_metni=karar_metni,
karar_no=karar_no,
basvuran=basvuran,
idare_adi=idare_adi,
baslangic_tarihi=baslangic_tarihi,
bitis_tarihi=bitis_tarihi
)
# Update security headers for this request
headers = {**self.http_client.headers, **self._generate_security_headers()}
# Get the appropriate endpoint for this decision type
endpoint = self.ENDPOINTS[decision_type]
# Make API request
response = await self.http_client.post(
endpoint,
json=payload.model_dump(),
headers=headers
)
response.raise_for_status()
response_data = response.json()
logger.debug(f"KikV2ApiClient: Raw API response structure: {type(response_data)}")
# Parse the API response based on decision type
if decision_type == KikV2DecisionType.UYUSMAZLIK:
api_response = KikV2SearchResponse(**response_data)
result_data = api_response.SorgulaKurulKararlariResponse.SorgulaKurulKararlariResult
elif decision_type == KikV2DecisionType.DUZENLEYICI:
api_response = KikV2SearchResponseDk(**response_data)
result_data = api_response.SorgulaKurulKararlariDkResponse.SorgulaKurulKararlariDkResult
elif decision_type == KikV2DecisionType.MAHKEME:
api_response = KikV2SearchResponseMk(**response_data)
result_data = api_response.SorgulaKurulKararlariMkResponse.SorgulaKurulKararlariMkResult
else:
raise ValueError(f"Unsupported decision type: {decision_type}")
# Check for API errors
if result_data.hataKodu and result_data.hataKodu != "0":
logger.warning(f"KikV2ApiClient: API returned error - Code: {result_data.hataKodu}, Message: {result_data.hataMesaji}")
return KikV2SearchResult(
decisions=[],
total_records=0,
page=1,
error_code=result_data.hataKodu,
error_message=result_data.hataMesaji
)
# Convert to compact format
compact_decisions = []
total_count = 0
for decision_group in result_data.KurulKararTutanakDetayListesi:
for decision_detail in decision_group.KurulKararTutanakDetayi:
compact_decision = KikV2CompactDecision(
kararNo=decision_detail.kararNo,
kararTarihi=decision_detail.kararTarihi,
basvuran=decision_detail.basvuran,
idareAdi=decision_detail.idareAdi,
basvuruKonusu=decision_detail.basvuruKonusu,
gundemMaddesiId=decision_detail.gundemMaddesiId,
decision_type=decision_type.value
)
compact_decisions.append(compact_decision)
total_count += 1
logger.info(f"KikV2ApiClient: Found {total_count} decisions")
return KikV2SearchResult(
decisions=compact_decisions,
total_records=total_count,
page=1,
error_code="0",
error_message=""
)
except httpx.HTTPStatusError as e:
logger.error(f"KikV2ApiClient: HTTP error during search: {e.response.status_code} - {e.response.text}")
return KikV2SearchResult(
decisions=[],
total_records=0,
page=1,
error_code="HTTP_ERROR",
error_message=f"HTTP {e.response.status_code}: {e.response.text}"
)
except Exception as e:
logger.error(f"KikV2ApiClient: Unexpected error during search: {str(e)}")
return KikV2SearchResult(
decisions=[],
total_records=0,
page=1,
error_code="UNEXPECTED_ERROR",
error_message=str(e)
)
async def get_document_markdown(self, document_id: str) -> KikV2DocumentMarkdown:
"""
Get KİK decision document content in Markdown format.
This method uses a two-step process:
1. Call GetSorgulamaUrl endpoint to get the actual document URL
2. Use Playwright to navigate to that URL and extract content
Args:
document_id: The gundemMaddesiId from search results
Returns:
KikV2DocumentMarkdown with document content converted to Markdown
"""
logger.info(f"KikV2ApiClient: Getting document for ID: {document_id}")
if not document_id or not document_id.strip():
return KikV2DocumentMarkdown(
document_id=document_id,
kararNo="",
markdown_content="",
source_url="",
error_message="Document ID is required"
)
try:
# Step 1: Get the actual document URL using GetSorgulamaUrl endpoint
logger.info(f"KikV2ApiClient: Step 1 - Getting document URL for ID: {document_id}")
# Update security headers for this request
headers = {**self.http_client.headers, **self._generate_security_headers()}
# Call GetSorgulamaUrl to get the real document URL
url_payload = {"sorguSayfaTipi": 2} # As shown in curl example
url_response = await self.http_client.post(
"/b_ihalearaclari/api/KurulKararlari/GetSorgulamaUrl",
json=url_payload,
headers=headers
)
url_response.raise_for_status()
url_data = url_response.json()
# Get the base document URL from API response
base_document_url = url_data.get("sorgulamaUrl", "")
if not base_document_url:
return KikV2DocumentMarkdown(
document_id=document_id,
kararNo="",
markdown_content="",
source_url="",
error_message="Could not get document URL from GetSorgulamaUrl API"
)
# Construct full document URL with the actual document ID
document_url = f"{base_document_url}?KararId={document_id}"
logger.info(f"KikV2ApiClient: Step 2 - Retrieved document URL: {document_url}")
except Exception as e:
logger.error(f"KikV2ApiClient: Error getting document URL for ID {document_id}: {str(e)}")
# Fallback to old method if GetSorgulamaUrl fails
document_url = f"https://ekap.kik.gov.tr/EKAP/Vatandas/KurulKararGoster.aspx?KararId={document_id}"
logger.info(f"KikV2ApiClient: Falling back to direct URL: {document_url}")
try:
# Step 2: Use Playwright to get the actual document content
logger.info(f"KikV2ApiClient: Step 2 - Using Playwright to retrieve document from: {document_url}")
try:
from playwright.async_api import async_playwright
async with async_playwright() as p:
# Launch browser
browser = await p.chromium.launch(
headless=True,
args=['--no-sandbox', '--disable-dev-shm-usage']
)
page = await browser.new_page(
user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"
)
# Navigate to document page with longer timeout for JS loading
await page.goto(document_url, wait_until="networkidle", timeout=15000)
# Wait for the document content to load (KİK pages might need more time for JS execution)
await page.wait_for_timeout(3000)
# Wait for Angular/Zone.js to finish loading and document to be ready
try:
# Wait for Angular zone to be available (this JavaScript code you showed)
await page.wait_for_function(
"typeof Zone !== 'undefined' && Zone.current",
timeout=10000
)
# Wait for network to be idle after Angular bootstrap
await page.wait_for_load_state("networkidle", timeout=10000)
# Wait for specific document content to appear
await page.wait_for_function(
"""
document.body.textContent.length > 5000 &&
(document.body.textContent.includes('Karar') ||
document.body.textContent.includes('KURUL') ||
document.body.textContent.includes('Gündem') ||
document.body.textContent.includes('Toplantı'))
""",
timeout=15000
)
logger.info("KikV2ApiClient: Angular document content loaded successfully")
except Exception as e:
logger.warning(f"KikV2ApiClient: Angular content loading timed out, proceeding anyway: {str(e)}")
# Give a bit more time for any remaining content to load
await page.wait_for_timeout(5000)
# Get page content
html_content = await page.content()
await browser.close()
logger.info(f"KikV2ApiClient: Retrieved content via Playwright, length: {len(html_content)}")
except ImportError:
logger.info("KikV2ApiClient: Playwright not available, falling back to httpx")
# Fallback to httpx
response = await self.http_client.get(
document_url,
headers={
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "tr,en-US;q=0.5",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36",
"Referer": "https://ekap.kik.gov.tr/",
"Cache-Control": "no-cache"
}
)
response.raise_for_status()
html_content = response.text
# Convert HTML to Markdown using MarkItDown with BytesIO
try:
from markitdown import MarkItDown
from io import BytesIO
md = MarkItDown()
html_bytes = html_content.encode('utf-8')
html_stream = BytesIO(html_bytes)
result = md.convert_stream(html_stream, file_extension=".html")
markdown_content = result.text_content
return KikV2DocumentMarkdown(
document_id=document_id,
kararNo="",
markdown_content=markdown_content,
source_url=document_url,
error_message=""
)
except ImportError:
return KikV2DocumentMarkdown(
document_id=document_id,
kararNo="",
markdown_content="MarkItDown library not available",
source_url=document_url,
error_message="MarkItDown library not installed"
)
except Exception as e:
logger.error(f"KikV2ApiClient: Error retrieving document {document_id}: {str(e)}")
return KikV2DocumentMarkdown(
document_id=document_id,
kararNo="",
markdown_content="",
source_url=document_url,
error_message=str(e)
)
async def close_client_session(self):
"""Close HTTP client session."""
await self.http_client.aclose()
logger.info("KikV2ApiClient: HTTP client session closed.")
+147
View File
@@ -0,0 +1,147 @@
# kik_mcp_module/models_v2.py
from pydantic import BaseModel, Field, ConfigDict
from typing import List, Optional
from datetime import datetime
from enum import Enum
# New KIK v2 API Models
class KikV2DecisionType(str, Enum):
"""KIK v2 Decision Types with corresponding endpoints."""
UYUSMAZLIK = "uyusmazlik" # Disputes - GetKurulKararlari
DUZENLEYICI = "duzenleyici" # Regulatory - GetKurulKararlariDk
MAHKEME = "mahkeme" # Court - GetKurulKararlariMk
class KikV2SearchRequest(BaseModel):
"""Model for KIK v2 API search request."""
KararMetni: str = Field("", description="Decision text search query")
KararNo: str = Field("", description="Decision number (e.g., '2025/UH.II-1801')")
BasvuranAdi: str = Field("", description="Applicant name")
IdareAdi: str = Field("", description="Administration name")
BaslangicTarihi: str = Field("", description="Start date (YYYY-MM-DD)")
BitisTarihi: str = Field("", description="End date (YYYY-MM-DD)")
class KikV2KeyValuePair(BaseModel):
"""Key-value pair for KIK v2 API request."""
key: str
value: str
class KikV2QueryRequest(BaseModel):
"""Nested query structure for KIK v2 API."""
keyValueOfstringanyType: List[KikV2KeyValuePair]
class KikV2RequestData(BaseModel):
"""Main request data structure for KIK v2 API."""
keyValuePairs: KikV2QueryRequest
# Request Payloads for different decision types
class KikV2SearchPayload(BaseModel):
"""Complete payload for KIK v2 API search - Uyuşmazlık (Disputes)."""
sorgulaKurulKararlari: KikV2RequestData
class KikV2SearchPayloadDk(BaseModel):
"""Complete payload for KIK v2 API search - Düzenleyici (Regulatory)."""
sorgulaKurulKararlariDk: KikV2RequestData
class KikV2SearchPayloadMk(BaseModel):
"""Complete payload for KIK v2 API search - Mahkeme (Court)."""
sorgulaKurulKararlariMk: KikV2RequestData
# Response Models
class KikV2DecisionDetail(BaseModel):
"""Individual decision detail from KIK v2 API response."""
resmiGazeteMukerrerSayi: str = Field("", description="Official Gazette duplicate number")
itiraz: str = Field("", description="Objection")
yayinlanmaTarihi: str = Field("", description="Publication date")
idareAdi: str = Field("", description="Administration name")
uzmanTCKN: str = Field("", description="Expert TCKN")
resmiGazeteTarihi: str = Field("", description="Official Gazette date")
basvuruKonusu: str = Field("", description="Application subject")
kararTurKod: str = Field("", description="Decision type code")
kararTurAciklama: str = Field("", description="Decision type description")
karar: str = Field("", description="Decision text")
kararNo: str = Field("", description="Decision number")
resmiGazeteSayisi: str = Field("", description="Official Gazette number")
inceleme: str = Field("", description="Review")
basvuruTarihi: str = Field("", description="Application date")
kararNitelikKod: str = Field("", description="Decision nature code")
resmiGazeteMukerrer: str = Field("", description="Official Gazette duplicate")
basvuruSayisi: str = Field("", description="Application number")
basvuran: str = Field("", description="Applicant")
kararNitelik: str = Field("", description="Decision nature")
uyusmazlikKararNo: str = Field("", description="Dispute decision number")
kurulNo: str = Field("", description="Board number")
gundemMaddesiSiraNo: str = Field("", description="Agenda item sequence")
kararTarihi: str = Field("", description="Decision date (ISO format)")
dosyaBirimKodu: str = Field("", description="File unit code")
gundemMaddesiId: str = Field("", description="Agenda item ID")
class KikV2DecisionGroup(BaseModel):
"""Group of decision details."""
KurulKararTutanakDetayi: List[KikV2DecisionDetail] = Field(alias="kurulKararTutanakDetayi")
model_config = ConfigDict(populate_by_name=True)
class KikV2SearchResultData(BaseModel):
"""Search result data structure."""
hataKodu: str = Field("", description="Error code")
hataMesaji: str = Field("", description="Error message")
KurulKararTutanakDetayListesi: List[KikV2DecisionGroup]
model_config = ConfigDict(populate_by_name=True)
class KikV2SearchResultWrapper(BaseModel):
"""Wrapper for search result."""
SorgulaKurulKararlariResult: KikV2SearchResultData
# Base Response Models
class KikV2SearchResponse(BaseModel):
"""Complete KIK v2 API search response for Uyuşmazlık (Disputes)."""
SorgulaKurulKararlariResponse: KikV2SearchResultWrapper
# Düzenleyici Kararlar (Regulatory Decisions) Response Models
class KikV2SearchResultWrapperDk(BaseModel):
"""Wrapper for regulatory decisions search result."""
SorgulaKurulKararlariDkResult: KikV2SearchResultData
class KikV2SearchResponseDk(BaseModel):
"""Complete KIK v2 API search response for Düzenleyici (Regulatory) decisions."""
SorgulaKurulKararlariDkResponse: KikV2SearchResultWrapperDk
# Mahkeme Kararlar (Court Decisions) Response Models
class KikV2SearchResultWrapperMk(BaseModel):
"""Wrapper for court decisions search result."""
SorgulaKurulKararlariMkResult: KikV2SearchResultData
class KikV2SearchResponseMk(BaseModel):
"""Complete KIK v2 API search response for Mahkeme (Court) decisions."""
SorgulaKurulKararlariMkResponse: KikV2SearchResultWrapperMk
# Simplified Models for MCP Tools
class KikV2CompactDecision(BaseModel):
"""Compact decision format for MCP tool responses."""
kararNo: str = Field("", description="Decision number")
kararTarihi: str = Field("", description="Decision date")
basvuran: str = Field("", description="Applicant")
idareAdi: str = Field("", description="Administration")
basvuruKonusu: str = Field("", description="Application subject")
gundemMaddesiId: str = Field("", description="Document ID for retrieval")
decision_type: str = Field("", description="Decision type (uyusmazlik/duzenleyici/mahkeme)")
class KikV2SearchResult(BaseModel):
"""Compact search results for MCP tools."""
decisions: List[KikV2CompactDecision]
total_records: int = Field(0, description="Total number of decisions found")
page: int = Field(1, description="Current page number")
error_code: str = Field("", description="API error code")
error_message: str = Field("", description="API error message")
class KikV2DocumentMarkdown(BaseModel):
"""Document content in Markdown format."""
document_id: str = Field("", description="Document ID")
kararNo: str = Field("", description="Decision number")
markdown_content: str = Field("", description="Decision content in Markdown")
source_url: str = Field("", description="Source URL")
error_message: str = Field("", description="Error message if retrieval failed")
+207 -149
View File
@@ -7,11 +7,18 @@ import httpx
import json import json
import time import time
from collections import defaultdict from collections import defaultdict
from pydantic import HttpUrl, Field from pydantic import BaseModel, HttpUrl, Field
from typing import Optional, Dict, List, Literal, Any, Union from typing import Optional, Dict, List, Literal, Any, Union
import urllib.parse import urllib.parse
import tiktoken
from fastmcp.server.middleware import Middleware, MiddlewareContext from fastmcp.server.middleware import Middleware, MiddlewareContext
# Optional tiktoken import for token counting
try:
import tiktoken
TIKTOKEN_AVAILABLE = True
except ImportError:
TIKTOKEN_AVAILABLE = False
tiktoken = None
from fastmcp.server.dependencies import get_access_token, AccessToken from fastmcp.server.dependencies import get_access_token, AccessToken
from fastmcp import Context from fastmcp import Context
@@ -21,24 +28,13 @@ class ToolError(Exception):
pass pass
# --- Logging Configuration Start --- # --- Logging Configuration Start ---
LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
if not os.path.exists(LOG_DIRECTORY):
os.makedirs(LOG_DIRECTORY)
LOG_FILE_PATH = os.path.join(LOG_DIRECTORY, "mcp_server.log")
root_logger = logging.getLogger() root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG) root_logger.setLevel(logging.INFO)
log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(threadName)s - %(message)s')
file_handler = logging.FileHandler(LOG_FILE_PATH, mode='a', encoding='utf-8')
file_handler.setFormatter(log_formatter)
file_handler.setLevel(logging.DEBUG)
root_logger.addHandler(file_handler)
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
console_handler.setFormatter(log_formatter) console_handler.setFormatter(log_formatter)
console_handler.setLevel(logging.INFO) console_handler.setLevel(logging.INFO)
root_logger.addHandler(console_handler) root_logger.addHandler(console_handler)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -50,22 +46,17 @@ class TokenCountingMiddleware(Middleware):
def __init__(self, model: str = "cl100k_base"): def __init__(self, model: str = "cl100k_base"):
"""Initialize token counting middleware. """Initialize token counting middleware.
Args: Args:
model: Tiktoken model name (cl100k_base for GPT-4/Claude compatibility) model: Tiktoken model name (cl100k_base for GPT-4/Claude compatibility)
""" """
if not TIKTOKEN_AVAILABLE:
raise ImportError("tiktoken is required for token counting. Install with: pip install tiktoken")
self.encoder = tiktoken.get_encoding(model) self.encoder = tiktoken.get_encoding(model)
self.model = model self.model = model
self.token_stats = defaultdict(lambda: {"input": 0, "output": 0, "calls": 0}) self.token_stats = defaultdict(lambda: {"input": 0, "output": 0, "calls": 0})
self.logger = logging.getLogger("token_counter") 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) self.logger.setLevel(logging.INFO)
def count_tokens(self, text: str) -> int: def count_tokens(self, text: str) -> int:
@@ -247,9 +238,14 @@ def create_app(auth=None):
else: else:
logger.info("MCP server created with standard capabilities...") logger.info("MCP server created with standard capabilities...")
token_counter = TokenCountingMiddleware() # Add token counting middleware only if tiktoken is available
app.add_middleware(token_counter) if TIKTOKEN_AVAILABLE:
logger.info("Token counting middleware added to MCP server") try:
token_counter = TokenCountingMiddleware()
app.add_middleware(token_counter)
logger.info("Token counting middleware added to MCP server")
except Exception as e:
logger.warning(f"Failed to add token counting middleware: {e}")
return app return app
@@ -294,13 +290,12 @@ from anayasa_mcp_module.models import (
AnayasaUnifiedDocumentMarkdown, AnayasaUnifiedDocumentMarkdown,
# Removed enum imports - now using Literal strings in models # Removed enum imports - now using Literal strings in models
) )
# KIK Module Imports # KIK v2 Module Imports (New API)
from kik_mcp_module.client import KikApiClient from kik_mcp_module.client_v2 import KikV2ApiClient
from kik_mcp_module.models import ( from kik_mcp_module.models_v2 import KikV2DecisionType
KikKararTipi, from kik_mcp_module.models_v2 import (
KikSearchRequest, KikV2SearchResult,
KikSearchResult, KikV2DocumentMarkdown
KikDocumentMarkdown
) )
from rekabet_mcp_module.client import RekabetKurumuApiClient from rekabet_mcp_module.client import RekabetKurumuApiClient
@@ -359,7 +354,7 @@ uyusmazlik_client_instance = UyusmazlikApiClient()
anayasa_norm_client_instance = AnayasaMahkemesiApiClient() anayasa_norm_client_instance = AnayasaMahkemesiApiClient()
anayasa_bireysel_client_instance = AnayasaBireyselBasvuruApiClient() anayasa_bireysel_client_instance = AnayasaBireyselBasvuruApiClient()
anayasa_unified_client_instance = AnayasaUnifiedClient() anayasa_unified_client_instance = AnayasaUnifiedClient()
kik_client_instance = KikApiClient() kik_v2_client_instance = KikV2ApiClient()
rekabet_client_instance = RekabetKurumuApiClient() rekabet_client_instance = RekabetKurumuApiClient()
bedesten_client_instance = BedestenApiClient() bedesten_client_instance = BedestenApiClient()
sayistay_client_instance = SayistayApiClient() sayistay_client_instance = SayistayApiClient()
@@ -624,7 +619,7 @@ async def search_emsal_detailed_decisions(
sort_direction: str = Field("desc", description="Sorting direction ('asc' or 'desc')."), sort_direction: str = Field("desc", description="Sorting direction ('asc' or 'desc')."),
page_number: int = Field(1, ge=1, description="Page number (accepts int)."), page_number: int = Field(1, ge=1, description="Page number (accepts int)."),
# page_size: int = Field(10, ge=1, le=10, description="Results per page.") # page_size: int = Field(10, ge=1, le=10, description="Results per page.")
) -> CompactEmsalSearchResult: ) -> Dict[str, Any]:
"""Search Emsal precedent decisions with detailed criteria.""" """Search Emsal precedent decisions with detailed criteria."""
page_size = 10 # Default value page_size = 10 # Default value
@@ -657,9 +652,9 @@ async def search_emsal_detailed_decisions(
total_records=api_response.data.recordsTotal if api_response.data.recordsTotal is not None else 0, total_records=api_response.data.recordsTotal if api_response.data.recordsTotal is not None else 0,
requested_page=search_query.page_number, requested_page=search_query.page_number,
page_size=search_query.page_size page_size=search_query.page_size
) ).model_dump()
logger.warning("API response for Emsal search did not contain expected data structure.") logger.warning("API response for Emsal search did not contain expected data structure.")
return CompactEmsalSearchResult(decisions=[], total_records=0, requested_page=search_query.page_number, page_size=search_query.page_size) return CompactEmsalSearchResult(decisions=[], total_records=0, requested_page=search_query.page_number, page_size=search_query.page_size).model_dump()
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_emsal_detailed_decisions'.") logger.exception(f"Error in tool 'search_emsal_detailed_decisions'.")
raise raise
@@ -671,12 +666,13 @@ async def search_emsal_detailed_decisions(
"idempotentHint": True "idempotentHint": True
} }
) )
async def get_emsal_document_markdown(id: str) -> EmsalDocumentMarkdown: async def get_emsal_document_markdown(id: str) -> Dict[str, Any]:
"""Get document as Markdown.""" """Get document as Markdown."""
logger.info(f"Tool 'get_emsal_document_markdown' called for ID: {id}") logger.info(f"Tool 'get_emsal_document_markdown' called for ID: {id}")
if not id or not id.strip(): raise ValueError("Document ID required for Emsal.") if not id or not id.strip(): raise ValueError("Document ID required for Emsal.")
try: try:
return await emsal_client_instance.get_decision_document_as_markdown(id) result = await emsal_client_instance.get_decision_document_as_markdown(id)
return result.model_dump()
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_emsal_document_markdown'.") logger.exception(f"Error in tool 'get_emsal_document_markdown'.")
raise raise
@@ -709,7 +705,7 @@ async def search_uyusmazlik_decisions(
hepsi: str = Field("", description="Search for texts containing all specified words."), hepsi: str = Field("", description="Search for texts containing all specified words."),
herhangi_birisi: str = Field("", description="Search for texts containing any of the specified words."), herhangi_birisi: str = Field("", description="Search for texts containing any of the specified words."),
not_hepsi: str = Field("", description="Exclude texts containing these specified words.") not_hepsi: str = Field("", description="Exclude texts containing these specified words.")
) -> UyusmazlikSearchResponse: ) -> Dict[str, Any]:
"""Search Court of Jurisdictional Disputes decisions.""" """Search Court of Jurisdictional Disputes decisions."""
# Convert string literals to enums # Convert string literals to enums
@@ -748,7 +744,8 @@ async def search_uyusmazlik_decisions(
logger.info(f"Tool 'search_uyusmazlik_decisions' called.") logger.info(f"Tool 'search_uyusmazlik_decisions' called.")
try: try:
return await uyusmazlik_client_instance.search_decisions(search_params) result = await uyusmazlik_client_instance.search_decisions(search_params)
return result.model_dump()
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_uyusmazlik_decisions'.") logger.exception(f"Error in tool 'search_uyusmazlik_decisions'.")
raise raise
@@ -762,13 +759,14 @@ async def search_uyusmazlik_decisions(
) )
async def get_uyusmazlik_document_markdown_from_url( async def get_uyusmazlik_document_markdown_from_url(
document_url: str = Field(..., description="Full URL to the Uyuşmazlık Mahkemesi decision document from search results") document_url: str = Field(..., description="Full URL to the Uyuşmazlık Mahkemesi decision document from search results")
) -> UyusmazlikDocumentMarkdown: ) -> Dict[str, Any]:
"""Get Uyuşmazlık Mahkemesi decision as Markdown.""" """Get Uyuşmazlık Mahkemesi decision as Markdown."""
logger.info(f"Tool 'get_uyusmazlik_document_markdown_from_url' called for URL: {str(document_url)}") logger.info(f"Tool 'get_uyusmazlik_document_markdown_from_url' called for URL: {str(document_url)}")
if not document_url: if not document_url:
raise ValueError("Document URL (document_url) is required for Uyuşmazlık document retrieval.") raise ValueError("Document URL (document_url) is required for Uyuşmazlık document retrieval.")
try: try:
return await uyusmazlik_client_instance.get_decision_document_as_markdown(str(document_url)) result = await uyusmazlik_client_instance.get_decision_document_as_markdown(str(document_url))
return result.model_dump()
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url'.") logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url'.")
raise raise
@@ -887,101 +885,125 @@ async def get_anayasa_document_unified(
logger.exception(f"Error in tool 'get_anayasa_document_unified'.") logger.exception(f"Error in tool 'get_anayasa_document_unified'.")
raise raise
# --- MCP Tools for KIK (Kamu İhale Kurulu) --- # --- MCP Tools for KIK v2 (Kamu İhale Kurulu - New API) ---
@app.tool( @app.tool(
description="Search Public Procurement Authority (KİK) decisions for procurement law disputes", 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)",
annotations={ annotations={
"readOnlyHint": True, "readOnlyHint": True,
"openWorldHint": True, "openWorldHint": True,
"idempotentHint": True "idempotentHint": True
} }
) )
async def search_kik_decisions( async def search_kik_v2_decisions(
karar_tipi: Literal["rbUyusmazlik", "rbDuzenleyici", "rbMahkeme"] = Field("rbUyusmazlik", description="Type of KIK Decision."), decision_type: str = Field("uyusmazlik", description="Decision type: 'uyusmazlik' (disputes), 'duzenleyici' (regulatory), or 'mahkeme' (court decisions)"),
karar_no: str = Field("", description="Decision Number (e.g., '2024/UH.II-1766')."), karar_metni: str = Field("", description="Decision text search query"),
karar_tarihi_baslangic: str = Field("", description="Decision Date Start (DD.MM.YYYY)."), karar_no: str = Field("", description="Decision number (e.g., '2025/UH.II-1801')"),
karar_tarihi_bitis: str = Field("", description="Decision Date End (DD.MM.YYYY)."), basvuran: str = Field("", description="Applicant name"),
basvuru_sahibi: str = Field("", description="Applicant."), idare_adi: str = Field("", description="Administration/procuring entity name"),
ihaleyi_yapan_idare: str = Field("", description="Procuring Entity."), baslangic_tarihi: str = Field("", description="Start date (YYYY-MM-DD format, e.g., '2025-01-01')"),
basvuru_konusu_ihale: str = Field("", description="Tender subject of the application."), bitis_tarihi: str = Field("", description="End date (YYYY-MM-DD format, e.g., '2025-12-31')")
karar_metni: str = Field("", description="Decision text search. Supports: +word, -word, \"exact phrase\", OR/AND"), ) -> dict:
yil: str = Field("", description="Year of the decision."), """Search Public Procurement Authority (KİK) decisions using the new v2 API.
resmi_gazete_tarihi: str = Field("", description="Official Gazette Date (DD.MM.YYYY)."),
resmi_gazete_sayisi: str = Field("", description="Official Gazette Number."),
page: int = Field(1, ge=1, description="Results page number.")
) -> KikSearchResult:
"""Search Public Procurement Authority (KIK) decisions."""
# Convert string literal to enum This tool supports all three KİK decision types:
karar_tipi_enum = KikKararTipi(karar_tipi) - uyusmazlik: Disputes and conflicts in public procurement
- duzenleyici: Regulatory decisions and guidelines
- mahkeme: Court decisions and legal interpretations
search_query = KikSearchRequest( Each decision type uses its respective endpoint (GetKurulKararlari, GetKurulKararlariDk, GetKurulKararlariMk)
karar_tipi=karar_tipi_enum, and returns results with the decision_type field populated for identification.
karar_no=karar_no, """
karar_tarihi_baslangic=karar_tarihi_baslangic,
karar_tarihi_bitis=karar_tarihi_bitis, logger.info(f"Tool 'search_kik_v2_decisions' called with decision_type='{decision_type}', karar_metni='{karar_metni}', karar_no='{karar_no}'")
basvuru_sahibi=basvuru_sahibi,
ihaleyi_yapan_idare=ihaleyi_yapan_idare,
basvuru_konusu_ihale=basvuru_konusu_ihale,
karar_metni=karar_metni,
yil=yil,
resmi_gazete_tarihi=resmi_gazete_tarihi,
resmi_gazete_sayisi=resmi_gazete_sayisi,
page=page
)
logger.info(f"Tool 'search_kik_decisions' called.")
try: try:
api_response = await kik_client_instance.search_decisions(search_query) # Validate and convert decision type
page_param_for_log = search_query.page if hasattr(search_query, 'page') else 1 try:
if not api_response.decisions and api_response.total_records == 0 and page_param_for_log == 1: kik_decision_type = KikV2DecisionType(decision_type)
logger.warning(f"KIK search returned no decisions for query.") except ValueError:
return api_response return {
"decisions": [],
"total_records": 0,
"page": 1,
"error_code": "INVALID_DECISION_TYPE",
"error_message": f"Invalid decision type: {decision_type}. Valid options: uyusmazlik, duzenleyici, mahkeme"
}
api_response = await kik_v2_client_instance.search_decisions(
decision_type=kik_decision_type,
karar_metni=karar_metni,
karar_no=karar_no,
basvuran=basvuran,
idare_adi=idare_adi,
baslangic_tarihi=baslangic_tarihi,
bitis_tarihi=bitis_tarihi
)
# Convert to dictionary for MCP tool response
result = {
"decisions": [decision.model_dump() for decision in api_response.decisions],
"total_records": api_response.total_records,
"page": api_response.page,
"error_code": api_response.error_code,
"error_message": api_response.error_message
}
logger.info(f"KİK v2 {decision_type} search completed. Found {len(api_response.decisions)} decisions")
return result
except Exception as e: except Exception as e:
logger.exception(f"Error in KIK search tool 'search_kik_decisions'.") logger.exception(f"Error in KİK v2 {decision_type} search tool 'search_kik_v2_decisions'.")
current_page_val = search_query.page if hasattr(search_query, 'page') else 1 return {
return KikSearchResult(decisions=[], total_records=0, current_page=current_page_val) "decisions": [],
"total_records": 0,
"page": 1,
"error_code": "TOOL_ERROR",
"error_message": str(e)
}
@app.tool( @app.tool(
description="Get Public Procurement Authority (KİK) decision text in paginated Markdown format", description="Get Public Procurement Authority (KİK) decision document using the new v2 API (placeholder - full implementation pending)",
annotations={ annotations={
"readOnlyHint": True, "readOnlyHint": True,
"idempotentHint": True "idempotentHint": True
} }
) )
async def get_kik_document_markdown( async def get_kik_v2_document_markdown(
karar_id: str = Field(..., description="The Base64 encoded KIK decision identifier."), document_id: str = Field(..., description="Document ID (gundemMaddesiId from search results)")
page_number: int = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed). Default is 1.") ) -> dict:
) -> KikDocumentMarkdown: """Get KİK decision document using the v2 API."""
"""Get KIK decision as paginated Markdown."""
logger.info(f"Tool 'get_kik_document_markdown' called for KIK karar_id: {karar_id}, Markdown Page: {page_number}") logger.info(f"Tool 'get_kik_v2_document_markdown' called for document ID: {document_id}")
if not document_id or not document_id.strip():
return {
"document_id": document_id,
"kararNo": "",
"markdown_content": "",
"source_url": "",
"error_message": "Document ID is required and must be a non-empty string"
}
if not karar_id or not karar_id.strip():
logger.error("KIK Document retrieval: karar_id cannot be empty.")
return KikDocumentMarkdown(
retrieved_with_karar_id=karar_id,
error_message="karar_id is required and must be a non-empty string.",
current_page=page_number or 1,
total_pages=1,
is_paginated=False
)
current_page_to_fetch = page_number if page_number is not None and page_number >= 1 else 1
try: try:
return await kik_client_instance.get_decision_document_as_markdown( api_response = await kik_v2_client_instance.get_document_markdown(document_id)
karar_id_b64=karar_id,
page_number=current_page_to_fetch return {
) "document_id": api_response.document_id,
"kararNo": api_response.kararNo,
"markdown_content": api_response.markdown_content,
"source_url": api_response.source_url,
"error_message": api_response.error_message
}
except Exception as e: except Exception as e:
logger.exception(f"Error in KIK document retrieval tool 'get_kik_document_markdown' for karar_id: {karar_id}") logger.exception(f"Error in KİK v2 document retrieval tool for document ID: {document_id}")
return KikDocumentMarkdown( return {
retrieved_with_karar_id=karar_id, "document_id": document_id,
error_message=f"Tool-level error during KIK document retrieval: {str(e)}", "kararNo": "",
current_page=current_page_to_fetch, "markdown_content": "",
total_pages=1, "source_url": "",
is_paginated=False "error_message": f"Tool-level error during document retrieval: {str(e)}"
) }
@app.tool( @app.tool(
description="Search Competition Authority (Rekabet Kurumu) decisions for competition law and antitrust", description="Search Competition Authority (Rekabet Kurumu) decisions for competition law and antitrust",
annotations={ annotations={
@@ -1008,7 +1030,7 @@ async def search_rekabet_kurumu_decisions(
KararSayisi: str = Field("", description="Decision number (Karar Sayısı)."), KararSayisi: str = Field("", description="Decision number (Karar Sayısı)."),
KararTarihi: str = Field("", description="Decision date (Karar Tarihi), e.g., DD.MM.YYYY."), 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.") page: int = Field(1, ge=1, description="Page number to fetch for the results list.")
) -> RekabetSearchResult: ) -> Dict[str, Any]:
"""Search Competition Authority decisions.""" """Search Competition Authority decisions."""
karar_turu_guid_enum = KARAR_TURU_ADI_TO_GUID_ENUM_MAP.get(KararTuru) karar_turu_guid_enum = KARAR_TURU_ADI_TO_GUID_ENUM_MAP.get(KararTuru)
@@ -1033,10 +1055,11 @@ async def search_rekabet_kurumu_decisions(
logger.info(f"Tool 'search_rekabet_kurumu_decisions' called. Query: {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.info(f"Tool 'search_rekabet_kurumu_decisions' called. Query: {search_query.model_dump_json(exclude_none=True, indent=2)}")
try: try:
return await rekabet_client_instance.search_decisions(search_query) result = await rekabet_client_instance.search_decisions(search_query)
return result.model_dump()
except Exception as e: except Exception as e:
logger.exception("Error in tool 'search_rekabet_kurumu_decisions'.") logger.exception("Error in tool 'search_rekabet_kurumu_decisions'.")
return RekabetSearchResult(decisions=[], retrieved_page_number=page, total_records_found=0, total_pages=0) return RekabetSearchResult(decisions=[], retrieved_page_number=page, total_records_found=0, total_pages=0).model_dump()
@app.tool( @app.tool(
description="Get Competition Authority decision text in paginated Markdown format", description="Get Competition Authority decision text in paginated Markdown format",
@@ -1048,15 +1071,15 @@ async def search_rekabet_kurumu_decisions(
async def get_rekabet_kurumu_document( async def get_rekabet_kurumu_document(
karar_id: str = Field(..., description="GUID (kararId) of the Rekabet Kurumu decision. This ID is obtained from search results."), karar_id: str = Field(..., description="GUID (kararId) of the Rekabet Kurumu decision. This ID is obtained from search results."),
page_number: int = Field(1, ge=1, description="Requested page number for the Markdown content converted from PDF (1-indexed, accepts int). Default is 1.") page_number: int = Field(1, ge=1, description="Requested page number for the Markdown content converted from PDF (1-indexed, accepts int). Default is 1.")
) -> RekabetDocument: ) -> Dict[str, Any]:
"""Get Competition Authority decision as paginated Markdown.""" """Get Competition Authority decision as paginated Markdown."""
logger.info(f"Tool 'get_rekabet_kurumu_document' called. Karar ID: {karar_id}, Markdown Page: {page_number}") logger.info(f"Tool 'get_rekabet_kurumu_document' called. Karar ID: {karar_id}, Markdown Page: {page_number}")
current_page_to_fetch = page_number if page_number >= 1 else 1 current_page_to_fetch = page_number if page_number >= 1 else 1
try: try:
result = await rekabet_client_instance.get_decision_document(karar_id, page_number=current_page_to_fetch)
return await rekabet_client_instance.get_decision_document(karar_id, page_number=current_page_to_fetch) return result.model_dump()
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_rekabet_kurumu_document'. Karar ID: {karar_id}") logger.exception(f"Error in tool 'get_rekabet_kurumu_document'. Karar ID: {karar_id}")
raise raise
@@ -1118,6 +1141,18 @@ For best results, use exact phrases with quotes for legal terms."""),
pageSize = 10 # Default value pageSize = 10 # Default value
# Convert date formats if provided
# Accept formats: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS.000Z
if kararTarihiStart and not kararTarihiStart.endswith('Z'):
# Convert simple date format to ISO 8601 with timezone
if 'T' not in kararTarihiStart:
kararTarihiStart = f"{kararTarihiStart}T00:00:00.000Z"
if kararTarihiEnd and not kararTarihiEnd.endswith('Z'):
# Convert simple date format to ISO 8601 with timezone
if 'T' not in kararTarihiEnd:
kararTarihiEnd = f"{kararTarihiEnd}T23:59:59.999Z"
search_data = BedestenSearchData( search_data = BedestenSearchData(
pageSize=pageSize, pageSize=pageSize,
pageNumber=pageNumber, pageNumber=pageNumber,
@@ -1340,10 +1375,10 @@ async def search_sayistay_unified(
yargilama_dairesi: Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8"] = Field("ALL", description="Chamber selection (daire only)"), yargilama_dairesi: Literal["ALL", "1", "2", "3", "4", "5", "6", "7", "8"] = Field("ALL", description="Chamber selection (daire only)"),
hesap_yili: str = Field("", description="Account year (daire only)"), hesap_yili: str = Field("", description="Account year (daire only)"),
web_karar_metni: str = Field("", description="Decision text search (daire only)") web_karar_metni: str = Field("", description="Decision text search (daire only)")
) -> SayistayUnifiedSearchResult: ) -> Dict[str, Any]:
"""Search Sayıştay decisions across all three decision types with unified interface.""" """Search Sayıştay decisions across all three decision types with unified interface."""
logger.info(f"Tool 'search_sayistay_unified' called with decision_type={decision_type}") logger.info(f"Tool 'search_sayistay_unified' called with decision_type={decision_type}")
try: try:
search_request = SayistayUnifiedSearchRequest( search_request = SayistayUnifiedSearchRequest(
decision_type=decision_type, decision_type=decision_type,
@@ -1366,7 +1401,8 @@ async def search_sayistay_unified(
hesap_yili=hesap_yili, hesap_yili=hesap_yili,
web_karar_metni=web_karar_metni web_karar_metni=web_karar_metni
) )
return await sayistay_unified_client_instance.search_unified(search_request) result = await sayistay_unified_client_instance.search_unified(search_request)
return result.model_dump()
except Exception as e: except Exception as e:
logger.exception("Error in tool 'search_sayistay_unified'") logger.exception("Error in tool 'search_sayistay_unified'")
raise raise
@@ -1382,15 +1418,16 @@ async def search_sayistay_unified(
async def get_sayistay_document_unified( async def get_sayistay_document_unified(
decision_id: str = Field(..., description="Decision ID from search_sayistay_unified results"), decision_id: str = Field(..., description="Decision ID from search_sayistay_unified results"),
decision_type: Literal["genel_kurul", "temyiz_kurulu", "daire"] = Field(..., description="Decision type: genel_kurul, temyiz_kurulu, or daire") decision_type: Literal["genel_kurul", "temyiz_kurulu", "daire"] = Field(..., description="Decision type: genel_kurul, temyiz_kurulu, or daire")
) -> SayistayUnifiedDocumentMarkdown: ) -> Dict[str, Any]:
"""Get Sayıştay decision document as Markdown for any decision type.""" """Get Sayıştay decision document as Markdown for any decision type."""
logger.info(f"Tool 'get_sayistay_document_unified' called for ID: {decision_id}, type: {decision_type}") logger.info(f"Tool 'get_sayistay_document_unified' called for ID: {decision_id}, type: {decision_type}")
if not decision_id or not decision_id.strip(): if not decision_id or not decision_id.strip():
raise ValueError("Decision ID must be a non-empty string.") raise ValueError("Decision ID must be a non-empty string.")
try: try:
return await sayistay_unified_client_instance.get_document_unified(decision_id, decision_type) result = await sayistay_unified_client_instance.get_document_unified(decision_id, decision_type)
return result.model_dump()
except Exception as e: except Exception as e:
logger.exception("Error in tool 'get_sayistay_document_unified'") logger.exception("Error in tool 'get_sayistay_document_unified'")
raise raise
@@ -1414,7 +1451,7 @@ def perform_cleanup():
globals().get('anayasa_norm_client_instance'), globals().get('anayasa_norm_client_instance'),
globals().get('anayasa_bireysel_client_instance'), globals().get('anayasa_bireysel_client_instance'),
globals().get('anayasa_unified_client_instance'), globals().get('anayasa_unified_client_instance'),
globals().get('kik_client_instance'), globals().get('kik_v2_client_instance'),
globals().get('rekabet_client_instance'), globals().get('rekabet_client_instance'),
globals().get('bedesten_client_instance'), globals().get('bedesten_client_instance'),
globals().get('sayistay_client_instance'), globals().get('sayistay_client_instance'),
@@ -1615,22 +1652,22 @@ async def search_kvkk_decisions(
keywords: str = Field(..., description="Turkish keywords. Supports +required -excluded \"exact phrase\" operators"), 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)."), 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).") # pageSize: int = Field(10, ge=1, le=20, description="Number of results per page (1-20).")
) -> KvkkSearchResult: ) -> Dict[str, Any]:
"""Search function for legal decisions.""" """Search function for legal decisions."""
logger.info(f"KVKK search tool called with keywords: {keywords}") logger.info(f"KVKK search tool called with keywords: {keywords}")
pageSize = 10 # Default value pageSize = 10 # Default value
search_request = KvkkSearchRequest( search_request = KvkkSearchRequest(
keywords=keywords, keywords=keywords,
page=page, page=page,
pageSize=pageSize pageSize=pageSize
) )
try: try:
result = await kvkk_client_instance.search_decisions(search_request) result = await kvkk_client_instance.search_decisions(search_request)
logger.info(f"KVKK search completed. Found {len(result.decisions)} decisions on page {page}") logger.info(f"KVKK search completed. Found {len(result.decisions)} decisions on page {page}")
return result return result.model_dump()
except Exception as e: except Exception as e:
logger.exception(f"Error in KVKK search: {e}") logger.exception(f"Error in KVKK search: {e}")
# Return empty result on error # Return empty result on error
@@ -1640,7 +1677,7 @@ async def search_kvkk_decisions(
page=page, page=page,
pageSize=pageSize, pageSize=pageSize,
query=keywords query=keywords
) ).model_dump()
@app.tool( @app.tool(
description="Get KVKK decision document in Markdown format with metadata extraction", description="Get KVKK decision document in Markdown format with metadata extraction",
@@ -1653,10 +1690,10 @@ async def search_kvkk_decisions(
async def get_kvkk_document_markdown( async def get_kvkk_document_markdown(
decision_url: str = Field(..., description="KVKK decision URL from search results"), decision_url: str = Field(..., description="KVKK decision URL from search results"),
page_number: int = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed, accepts int). Default is 1 (first 5,000 characters).") page_number: int = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed, accepts int). Default is 1 (first 5,000 characters).")
) -> KvkkDocumentMarkdown: ) -> Dict[str, Any]:
"""Get KVKK decision as paginated Markdown.""" """Get KVKK decision as paginated Markdown."""
logger.info(f"KVKK document retrieval tool called for URL: {decision_url}") logger.info(f"KVKK document retrieval tool called for URL: {decision_url}")
if not decision_url or not decision_url.strip(): if not decision_url or not decision_url.strip():
return KvkkDocumentMarkdown( return KvkkDocumentMarkdown(
source_url=HttpUrl("https://www.kvkk.gov.tr"), source_url=HttpUrl("https://www.kvkk.gov.tr"),
@@ -1669,7 +1706,7 @@ async def get_kvkk_document_markdown(
total_pages=0, total_pages=0,
is_paginated=False, is_paginated=False,
error_message="Decision URL is required and cannot be empty." error_message="Decision URL is required and cannot be empty."
) ).model_dump()
try: try:
# Validate URL format # Validate URL format
@@ -1685,11 +1722,11 @@ async def get_kvkk_document_markdown(
total_pages=0, total_pages=0,
is_paginated=False, is_paginated=False,
error_message="Invalid KVKK decision URL format. URL must start with https://www.kvkk.gov.tr/" error_message="Invalid KVKK decision URL format. URL must start with https://www.kvkk.gov.tr/"
) ).model_dump()
result = await kvkk_client_instance.get_decision_document(decision_url, page_number or 1) result = await kvkk_client_instance.get_decision_document(decision_url, page_number or 1)
logger.info(f"KVKK document retrieved successfully. Page {result.current_page}/{result.total_pages}, Content length: {len(result.markdown_chunk) if result.markdown_chunk else 0}") logger.info(f"KVKK document retrieved successfully. Page {result.current_page}/{result.total_pages}, Content length: {len(result.markdown_chunk) if result.markdown_chunk else 0}")
return result return result.model_dump()
except Exception as e: except Exception as e:
logger.exception(f"Error retrieving KVKK document: {e}") logger.exception(f"Error retrieving KVKK document: {e}")
@@ -1704,7 +1741,7 @@ async def get_kvkk_document_markdown(
total_pages=0, total_pages=0,
is_paginated=False, is_paginated=False,
error_message=f"Error retrieving KVKK document: {str(e)}" error_message=f"Error retrieving KVKK document: {str(e)}"
) ).model_dump()
# --- MCP Tools for BDDK (Banking Regulation Authority) --- # --- MCP Tools for BDDK (Banking Regulation Authority) ---
@app.tool( @app.tool(
@@ -1851,6 +1888,7 @@ def get_preview_text(markdown_content: str, skip_chars: int = 100, preview_chars
return preview.strip() return preview.strip()
@app.tool( @app.tool(
description="DO NOT USE unless you are ChatGPT Deep Research. Search Turkish courts (Turkish keywords only). Supports: +term (must have), -term (exclude), \"exact phrase\", term1 OR term2", description="DO NOT USE unless you are ChatGPT Deep Research. Search Turkish courts (Turkish keywords only). Supports: +term (must have), -term (exclude), \"exact phrase\", term1 OR term2",
annotations={ annotations={
@@ -1861,7 +1899,7 @@ def get_preview_text(markdown_content: str, skip_chars: int = 100, preview_chars
) )
async def search( async def search(
query: str = Field(..., description="Turkish search query") query: str = Field(..., description="Turkish search query")
) -> Dict[str, List[Dict[str, str]]]: ) -> Dict[str, Any]:
""" """
Bedesten API search tool for ChatGPT Deep Research compatibility. Bedesten API search tool for ChatGPT Deep Research compatibility.
@@ -1975,13 +2013,33 @@ async def search(
""" """
logger.info(f"ChatGPT Deep Research search completed. Found {len(results)} results via Bedesten API.") logger.info(f"ChatGPT Deep Research search completed. Found {len(results)} results via Bedesten API.")
return {"results": results} return {
"results": [
{
"id": item["id"],
"title": item["title"],
"text": item["text"],
"url": item["url"]
}
for item in results
]
}
except Exception as e: except Exception as e:
logger.exception("Error in ChatGPT Deep Research search tool") logger.exception("Error in ChatGPT Deep Research search tool")
# Return partial results if any were found # Return partial results if any were found
if results: if results:
return {"results": results} return {
"results": [
{
"id": item["id"],
"title": item["title"],
"text": item["text"],
"url": item["url"]
}
for item in results
]
}
raise raise
@app.tool( @app.tool(
@@ -2148,7 +2206,7 @@ def main():
app = create_app() app = create_app()
logger.info(f"Starting {app.name} server via main() function...") logger.info(f"Starting {app.name} server via main() function...")
logger.info(f"Logs will be written to: {LOG_FILE_PATH}") # logger.info(f"Logs will be written to: {LOG_FILE_PATH}") # File logging disabled
# Ensure Playwright browsers are installed # Ensure Playwright browsers are installed
ensure_playwright_browsers() ensure_playwright_browsers()
+3 -3
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "yargi-mcp" name = "yargi-mcp"
version = "0.1.6" version = "0.1.8"
description = "MCP Server For Turkish Legal Databases" description = "MCP Server For Turkish Legal Databases"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
@@ -29,8 +29,6 @@ dependencies = [
"fastmcp>=2.10.5", "fastmcp>=2.10.5",
"pypdf>=5.5.0", "pypdf>=5.5.0",
"fastapi>=0.115.14", "fastapi>=0.115.14",
"PyJWT>=2.8.0",
"tiktoken>=0.5.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]
@@ -50,6 +48,8 @@ saas = [
"clerk-backend-api>=3.0.0", "clerk-backend-api>=3.0.0",
"stripe>=9.1.0", "stripe>=9.1.0",
"upstash-redis>=1.1.0", "upstash-redis>=1.1.0",
"tiktoken>=0.5.0",
"PyJWT>=2.8.0",
] ]
[project.scripts] [project.scripts]
Generated
+2485
View File
File diff suppressed because it is too large Load Diff