19 Commits
Author SHA1 Message Date
saidsurucu 180c29e82e Update README.md 2025-05-27 22:40:33 +03:00
saidsurucu 957df46b7d add kik module 2025-05-27 22:16:05 +03:00
saidsurucu 9df6eba37c Update client.py 2025-05-24 02:37:24 +03:00
saidsurucu 94553c3b78 Update pyproject.toml 2025-05-24 02:33:04 +03:00
saidsurucu 539b0c7041 Update mcp_server_main.py 2025-05-24 02:12:20 +03:00
saidsurucu 4a2af434ab Update pyproject.toml 2025-05-24 02:09:32 +03:00
saidsurucu c1352c868d Create __main__.py 2025-05-24 02:07:36 +03:00
saidsurucu 2271423c91 Update pyproject.toml 2025-05-24 02:06:40 +03:00
saidsurucu d59b498be7 Update mcp_server_main.py 2025-05-24 02:04:56 +03:00
saidsurucu 3c10913680 Update pyproject.toml 2025-05-24 02:01:58 +03:00
saidsurucu 32c4d06eb6 Update mcp_server_main.py 2025-05-24 01:29:08 +03:00
saidsurucu f892d430cb Update pyproject.toml 2025-05-24 01:21:55 +03:00
saidsurucu b19ccc60d7 add pyproject.toml 2025-05-24 01:19:53 +03:00
saidsurucu f6e95140cf Update mcp_server_main.py 2025-05-24 01:18:45 +03:00
saidsurucu 4d86833fd6 Merge branch 'main' of https://github.com/saidsurucu/yargi-mcp 2025-05-23 16:51:27 +03:00
saidsurucu 81d82369da Update install.sh 2025-05-23 16:51:23 +03:00
saidsurucu ae9bdebdad Update README.md 2025-05-23 16:02:49 +03:00
saidsurucu f82dc9f493 Update README.md 2025-05-23 16:01:46 +03:00
saidsurucu 970dd65bce Merge pull request #1 from saidsurucu/install.bat
Install.bat
2025-05-23 15:44:25 +03:00
10 changed files with 820 additions and 91 deletions
+3 -1
View File
@@ -160,4 +160,6 @@ cython_debug/
#.idea/ #.idea/
.DS_Store .DS_Store
hello.py hello.py
*.toml
*.html
test_kik_client.py
+105 -8
View File
@@ -2,7 +2,7 @@
[![Star History Chart](https://api.star-history.com/svg?repos=saidsurucu/yargi-mcp&type=Date)](https://www.star-history.com/#saidsurucu/yargi-mcp&Date) [![Star History Chart](https://api.star-history.com/svg?repos=saidsurucu/yargi-mcp&type=Date)](https://www.star-history.com/#saidsurucu/yargi-mcp&Date)
Bu proje, çeşitli Türk hukuk kaynaklarına (Yargıtay, Danıştay, Emsal Kararlar, Uyuşmazlık Mahkemesi ve Anayasa Mahkemesi - Norm Denetimi ile Bireysel Başvuru Kararları) erişimi kolaylaştıran bir [FastMCP](https://gofastmcp.com/) sunucusu oluşturur. Bu sayede, bu kaynaklardan veri arama ve belge getirme işlemleri, Model Context Protocol (MCP) destekleyen LLM (Büyük Dil Modeli) uygulamaları (örneğin Claude Desktop) ve diğer istemciler tarafından araç (tool) olarak kullanılabilir hale gelir. Bu proje, çeşitli Türk hukuk kaynaklarına (Yargıtay, Danıştay, Emsal Kararlar, Uyuşmazlık Mahkemesi, Anayasa Mahkemesi - Norm Denetimi ile Bireysel Başvuru Kararları ve Kamu İhale Kurulu Kararları) erişimi kolaylaştıran bir [FastMCP](https://gofastmcp.com/) sunucusu oluşturur. Bu sayede, bu kaynaklardan veri arama ve belge getirme işlemleri, Model Context Protocol (MCP) destekleyen LLM (Büyük Dil Modeli) uygulamaları (örneğin Claude Desktop) ve diğer istemciler tarafından araç (tool) olarak kullanılabilir hale gelir.
![örnek](./ornek.png) ![örnek](./ornek.png)
@@ -16,13 +16,103 @@ Bu proje, çeşitli Türk hukuk kaynaklarına (Yargıtay, Danıştay, Emsal Kara
* **Uyuşmazlık Mahkemesi:** Form tabanlı kriterlerle karar arama ve karar metinlerini (URL ile erişilen) Markdown formatında getirme. * **Uyuşmazlık Mahkemesi:** Form tabanlı kriterlerle karar arama ve karar metinlerini (URL ile erişilen) Markdown formatında getirme.
* **Anayasa Mahkemesi (Norm Denetimi):** Kapsamlı kriterlerle norm denetimi kararlarını arama; uzun karar metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme. * **Anayasa Mahkemesi (Norm Denetimi):** Kapsamlı kriterlerle norm denetimi kararlarını arama; uzun karar metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme.
* **Anayasa Mahkemesi (Bireysel Başvuru):** Kapsamlı kriterlerle bireysel başvuru "Karar Arama Raporu" oluşturma ve listedeki kararların metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme. * **Anayasa Mahkemesi (Bireysel Başvuru):** Kapsamlı kriterlerle bireysel başvuru "Karar Arama Raporu" oluşturma ve listedeki kararların metinlerini (5.000 karakterlik) sayfalanmış Markdown formatında getirme.
* **KİK (Kamu İhale Kurulu):** Çeşitli kriterlerle Kurul kararlarını arama; uzun karar metinlerini (varsayılan 5.000 karakterlik) sayfalanmış Markdown formatında getirme (Playwright ile tarayıcı otomasyonu kullanılır).
* Karar metinlerinin daha kolay işlenebilmesi için Markdown formatına çevrilmesi. * Karar metinlerinin daha kolay işlenebilmesi için Markdown formatına çevrilmesi.
* Claude Desktop uygulaması ile `fastmcp install` komutu kullanılarak kolay entegrasyon. * Claude Desktop uygulaması ile `fastmcp install` komutu kullanılarak kolay entegrasyon.
---
📋 **Ön Gereksinimler** 📋 **Ön Gereksinimler**
* **Python Sürümü:** Python 3.10 veya daha yeni bir sürümünün sisteminizde kurulu olması gerekmektedir. Python'ı [python.org](https://www.python.org/) adresinden indirebilirsiniz. Bu Yargı MCP aracını Claude Desktop ile kullanabilmek için öncelikle aşağıdaki yazılımların sisteminizde kurulu olması gerekmektedir:
* **Paket Yöneticisi:** `pip` (Python ile birlikte gelir) veya tercihen `uv` ([Astral](https://astral.sh/uv) tarafından geliştirilen hızlı Python paket yöneticisi).
1. **Claude Desktop:** Henüz kurmadıysanız, [Claude Desktop web sitesinden](https://claude.ai/desktop) işletim sisteminize uygun sürümü indirip kurun.
2. **Python Sürümü:** **Python 3.11** sürümü tavsiye edilir. Python 3.12 ve üzeri yeni sürümler, bazı bağımlılıklarda (özellikle `playwright` ve ilişkili tarayıcı sürücüleri) belirli ortamlarda uyumluluk sorunlarına yol açabilir. Bu proje için 3.11 sürümü stabilite açısından önerilmektedir.
* **Windows Kullanıcıları:** Eğer Python kurulu değilse, [python.org/downloads/windows/](https://www.python.org/downloads/windows/) adresinden Python 3.11'in uygun bir sürümünü indirip kurabilirsiniz. Kurulum sırasında "**Add Python to PATH**" (Python'ı PATH'e ekle) seçeneğini işaretlemeyi unutmayın.
* **macOS Kullanıcıları:** macOS genellikle Python ile birlikte gelir. Terminal'de `python3 --version` yazarak kontrol edebilirsiniz. Eğer Python 3.11 değilse veya eski bir sürümse, [python.org](https://www.python.org/downloads/macos/) adresinden veya [Homebrew](https://brew.sh/) (`brew install python@3.11`) ile kurabilirsiniz.
* **Linux Kullanıcıları:** Çoğu Linux dağıtımı Python ile gelir. Terminal'de `python3 --version` yazarak kontrol edebilirsiniz. Gerekirse dağıtımınızın paket yöneticisi ile Python 3.11'i kurabilirsiniz (örn: `sudo apt update && sudo apt install python3.11 python3.11-pip python3.11-venv` veya dağıtımınıza uygun komutlar).
3. **Paket Yöneticisi:** `pip` (Python ile birlikte gelir) veya tercihen `uv` ([Astral](https://astral.sh/uv) tarafından geliştirilen hızlı Python paket yöneticisi). Kurulum script'lerimiz `uv`'yi sizin için kurmayı deneyecektir.
4. **Playwright Tarayıcıları:** KİK modülü Playwright kullandığı için, ilgili tarayıcıların kurulmuş olması gerekir. `KikApiClient` varsayılan olarak Chromium kullanır. Eğer Playwright veya tarayıcıları manuel kuracaksanız:
```bash
# Önce playwright kütüphanesini kurun (uv veya pip ile)
# uv pip install playwright
# pip install playwright
# Sonra tarayıcıları kurun (proje bağımlılıkları kurulduktan sonra da yapılabilir)
playwright install --with-deps chromium
# '--with-deps' chromium için gerekli işletim sistemi bağımlılıklarını da kurmaya çalışır.
```
Kurulum scriptleri (`install.bat`, `install.sh`, `install.py`) genellikle `playwright` Python kütüphanesini kurar. Tarayıcıların ayrıca `playwright install` ile kurulması gerekebilir; eğer sunucu başlatılırken KİK modülü hata verirse, bu adımı manuel olarak çalıştırmanız gerekebilir.
---
🚀 **Kolay Kurulum Adımları (Claude Desktop için)**
Bu bölüm, teknik bilgisi daha az olan kullanıcıların **Yargı MCP** araçlarını Claude Desktop uygulamalarına hızlı ve kolay bir şekilde entegre etmeleri için hazırlanmıştır.
**Öncelikle Yapılması Gerekenler:**
1. **Proje Dosyalarını İndirin:**
* Bu GitHub deposunun ana sayfasına gidin.
* Yeşil renkli "**<> Code**" düğmesine tıklayın.
* Açılan menüden "**Download ZIP**" seçeneğini seçin.
* İndirdiğiniz ZIP dosyasını bilgisayarınızda kolayca erişebileceğiniz bir klasöre çıkartın (örneğin, `Belgelerim` veya `Masaüstü` altında `yargi-mcp` adında bir klasör oluşturabilirsiniz).
Proje dosyalarını bilgisayarınıza aldıktan sonra, işletim sisteminize uygun kurulum script'ini çalıştırabilirsiniz.
### Windows Kullanıcıları İçin (`install.bat`)
1. Proje dosyalarını çıkarttığınız klasörün içine gidin (örneğin, `C:\Users\KULLANICIADINIZ\Documents\yargi-mcp` klasörü).
2. `install.bat` adlı dosyayı bulun.
3. Bu dosyaya **çift tıklayarak** çalıştırın.
4. Kurulum sırasında bir komut istemi penceresi açılacak ve bazı mesajlar göreceksiniz. Script, gerekli araçları (`uv`, `fastmcp` CLI, `playwright` ve tarayıcıları) sisteminize kurmayı deneyecek ve ardından "**Yargı MCP**" aracını Claude Desktop'a entegre edecektir.
* *Not: Bu işlem sırasında script sizden yönetici onayı isteyebilir veya internet bağlantısı gerektirebilir. `uv` kurulumu için PowerShell script çalıştırma politikalarınızda geçici bir değişiklik yapılması gerekebilir; script bunu sizin için halletmeye çalışacaktır.*
5. Kurulum tamamlandığında, komut istemi penceresinde bir başarı mesajı göreceksiniz. Pencere, "Devam etmek için bir tuşa basın..." mesajıyla açık kalacaktır. Herhangi bir tuşa basarak pencereyi kapatabilirsiniz.
6. **Önemli:** Kurulumun etkili olması için Claude Desktop uygulamasını tamamen kapatıp yeniden başlatmanız gerekebilir.
### macOS ve Linux Kullanıcıları İçin (`install.sh`)
1. Proje dosyalarını çıkarttığınız klasörün içine gidin (örneğin, `/Users/KULLANICIADINIZ/Documents/yargi-mcp` klasörü).
2. **Terminal** uygulamasını açın:
* **macOS'te:**
1. **Finder**'ı açın.
2. Proje dosyalarını çıkarttığınız klasöre (`yargi-mcp` gibi) gidin.
3. Finder penceresinin en altındaki yol çubuğunda (Path Bar), klasör adının üzerine **Control tuşuna basılı tutarak tıklayın** (veya sağ tıklayın).
4. Açılan menüden "**Terminal'de Aç**" seçeneğini seçin. (Eğer bu seçenek yoksa, Finder'da `Görünüm > Yol Çubuğunu Göster` seçeneğinin aktif olduğundan emin olun. Alternatif olarak, `Uygulamalar > İzlenceler > Terminal` yolunu izleyip `cd` komutuyla klasörünüze gidin.)
* **Linux'ta:** Genellikle dosya yöneticisinde klasöre sağ tıklayıp "Burada Terminal Aç" seçeneğini kullanabilir veya Ctrl+Alt+T kısayoluyla terminal açıp `cd` komutuyla klasörünüze gidebilirsiniz.
3. Terminalde, doğru klasörde olduğunuzdan emin olduktan sonra, script'e çalıştırma izni verin (bu işlemi sadece bir kez yapmanız yeterlidir):
```bash
chmod +x install.sh
```
4. Script'i çalıştırın:
```bash
./install.sh
```
5. Kurulum sırasında terminalde bazı mesajlar göreceksiniz. Script, gerekli araçları (`uv`, `fastmcp` CLI) sisteminize kurmayı deneyecek ve ardından "**Yargı MCP**" aracını Claude Desktop'a entegre edecektir.
* *Not: Bu işlem sırasında script sizden şifrenizi (`sudo` yetkileri için, özellikle `uv` kurulumunda) isteyebilir veya internet bağlantısı gerektirebilir.*
6. Kurulum tamamlandığında, terminalde bir başarı mesajı göreceksiniz.
7. **Önemli:** Kurulumun etkili olması için Claude Desktop uygulamasını tamamen kapatıp yeniden başlatmanız gerekebilir. Ayrıca, eğer `uv` veya `fastmcp` PATH'e yeni eklendiyse, terminalinizi de yeniden başlatmanız veya shell profilinizi (`~/.bashrc`, `~/.zshrc`, `~/.profile` vb.) `source ~/.zshrc` (veya kullandığınız shell'e uygun komutla) yeniden yüklemeniz gerekebilir. Script bu konuda sizi uyaracaktır.
### Python Script ile Kurulum (`install.py`) (Platform Bağımsız Alternatif)
Eğer yukarıdaki işletim sistemine özel script'lerde sorun yaşarsanız veya Python tabanlı bir kurulumu tercih ediyorsanız, `install.py` script'ini kullanabilirsiniz. Bu yöntem, sisteminizde Python 3.11'in ve `pip` paket yöneticisinin kurulu ve çalışır durumda olmasını gerektirir.
1. Proje dosyalarını çıkarttığınız klasörün içine Terminal veya Komut İstemi ile gidin.
2. Aşağıdaki komutu çalıştırın (sisteminizdeki Python 3 komutuna göre `python` veya `python3` kullanın):
```bash
python3 install.py
```
veya
```bash
python install.py
```
3. Script, size gerekli adımlarda rehberlik edecek ve kurulumu tamamlamaya çalışacaktır. Kurulum sırasında ek bağımlılıkların indirilmesi gerekebilir.
---
**Kurulum Sonrası**
Kurulum başarıyla tamamlandıktan sonra, Claude Desktop uygulamasını (gerekirse yeniden başlatarak) açın. Araçlar menüsünde (genellikle ekranın sağ alt köşesindeki çekiç 🛠️ simgesi altında) "**Yargı MCP**" adlı yeni aracı görmelisiniz.
Herhangi bir sorunla karşılaşırsanız, lütfen [GitHub Issues](https://github.com/saidsurucu/yargi-mcp/issues) bölümünden bize bildirin.
⚙️ **Kurulum Adımları (Claude Desktop Entegrasyonu Odaklı)** ⚙️ **Kurulum Adımları (Claude Desktop Entegrasyonu Odaklı)**
@@ -32,11 +122,11 @@ Claude Desktop uygulamasına yükleme yapabilmek için öncelikle `uv` (önerili
* **macOS ve Linux için:** * **macOS ve Linux için:**
```bash ```bash
curl -LsSf [https://astral.sh/uv/install.sh](https://astral.sh/uv/install.sh) | sh curl -LsSf https://astral.sh/uv/install.sh | sh
``` ```
* **Windows için (PowerShell kullanarak):** * **Windows için (PowerShell kullanarak):**
```powershell ```powershell
powershell -c "irm [https://astral.sh/uv/install.ps1](https://astral.sh/uv/install.ps1) | iex" powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
``` ```
* Kurulumdan sonra, `uv` komutunun sisteminiz tarafından tanınması için terminalinizi yeniden başlatmanız veya `PATH` ortam değişkeninizi güncellemeniz gerekebilir. `uv --version` komutu ile kurulumu doğrulayabilirsiniz. * Kurulumdan sonra, `uv` komutunun sisteminiz tarafından tanınması için terminalinizi yeniden başlatmanız veya `PATH` ortam değişkeninizi güncellemeniz gerekebilir. `uv --version` komutu ile kurulumu doğrulayabilirsiniz.
@@ -73,6 +163,7 @@ beautifulsoup4
markitdown markitdown
pydantic pydantic
aiohttp aiohttp
playwright
``` ```
(Eğer sunucuyu bağımsız olarak geliştirmek veya test etmek isterseniz, projenizin kök dizininde bir sanal ortam oluşturup örn: `uv venv` & `source .venv/bin/activate` bu bağımlılıkları `uv pip install -r requirements.txt` komutuyla kurabilirsiniz.) (Eğer sunucuyu bağımsız olarak geliştirmek veya test etmek isterseniz, projenizin kök dizininde bir sanal ortam oluşturup örn: `uv venv` & `source .venv/bin/activate` bu bağımlılıkları `uv pip install -r requirements.txt` komutuyla kurabilirsiniz.)
@@ -90,13 +181,14 @@ Yukarıdaki kurulum adımlarını tamamladıktan sonra, bu sunucuyu Claude Deskt
--with beautifulsoup4 \ --with beautifulsoup4 \
--with markitdown \ --with markitdown \
--with pydantic \ --with pydantic \
--with aiohttp --with aiohttp \
--with playwright
``` ```
* `--name "Yargı MCP"`: Araç Claude Desktop'ta bu isimle görünecektir. * `--name "Yargı MCP"`: Araç Claude Desktop'ta bu isimle görünecektir.
* `--with ...`: Sunucunun çalışması için gereken Python bağımlılıklarını belirtir. * `--with ...`: Sunucunun çalışması için gereken Python bağımlılıklarını belirtir.
Bu komut, `uv` kullanarak sunucunuz için izole bir Python ortamı oluşturacak, belirtilen bağımlılıkları kuracak ve aracı Claude Desktop uygulamasına kaydedecektir. Bu komut, `uv` kullanarak (eğer kuruluysa ve bulunabiliyorsa) sunucunuz için izole bir Python ortamı oluşturacak, belirtilen bağımlılıkları kuracak ve aracı Claude Desktop uygulamasına kaydedecektir. Playwright tarayıcılarının (`playwright install chromium` gibi) ayrıca kurulması gerekebilir.
⚙️ **Claude Desktop Manuel Kurulumu (Yapılandırma Dosyası ile - Alternatif)** ⚙️ **Claude Desktop Manuel Kurulumu (Yapılandırma Dosyası ile - Alternatif)**
@@ -119,6 +211,7 @@ Yukarıdaki kurulum adımlarını tamamladıktan sonra, bu sunucuyu Claude Deskt
"--with", "markitdown", "--with", "markitdown",
"--with", "pydantic", "--with", "pydantic",
"--with", "aiohttp", "--with", "aiohttp",
"--with", "playwright",
"--with", "fastmcp", "--with", "fastmcp",
"fastmcp", "run", "fastmcp", "run",
"/TAM/PROJE/YOLUNUZ/yargi-mcp/mcp_server_main.py" "/TAM/PROJE/YOLUNUZ/yargi-mcp/mcp_server_main.py"
@@ -134,6 +227,7 @@ Yukarıdaki kurulum adımlarını tamamladıktan sonra, bu sunucuyu Claude Deskt
Bu FastMCP sunucusu aşağıdaki temel araçları sunar: Bu FastMCP sunucusu aşağıdaki temel araçları sunar:
* **Yargıtay Araçları:** * **Yargıtay Araçları:**
* `search_yargitay_detailed(search_query: YargitayDetailedSearchRequest) -> CompactYargitaySearchResult`: Yargıtay kararlarını detaylı kriterlerle arar. * `search_yargitay_detailed(search_query: YargitayDetailedSearchRequest) -> CompactYargitaySearchResult`: Yargıtay kararlarını detaylı kriterlerle arar.
* `get_yargitay_document_markdown(document_id: str) -> YargitayDocumentMarkdown`: Belirli bir Yargıtay kararının metnini Markdown formatında getirir. * `get_yargitay_document_markdown(document_id: str) -> YargitayDocumentMarkdown`: Belirli bir Yargıtay kararının metnini Markdown formatında getirir.
@@ -159,7 +253,10 @@ Bu FastMCP sunucusu aşağıdaki temel araçları sunar:
* `search_anayasa_bireysel_basvuru_report(search_query: AnayasaBireyselReportSearchRequest) -> AnayasaBireyselReportSearchResult`: AYM Bireysel Başvuru "Karar Arama Raporu" oluşturur. * `search_anayasa_bireysel_basvuru_report(search_query: AnayasaBireyselReportSearchRequest) -> AnayasaBireyselReportSearchResult`: AYM Bireysel Başvuru "Karar Arama Raporu" oluşturur.
* `get_anayasa_bireysel_basvuru_document_markdown(document_url_path: str, page_number: Optional[int] = 1) -> AnayasaBireyselBasvuruDocumentMarkdown`: Belirli bir AYM Bireysel Başvuru kararını URL path'inden alır ve 5.000 karakterlik sayfalanmış Markdown içeriğini getirir. * `get_anayasa_bireysel_basvuru_document_markdown(document_url_path: str, page_number: Optional[int] = 1) -> AnayasaBireyselBasvuruDocumentMarkdown`: Belirli bir AYM Bireysel Başvuru kararını URL path'inden alır ve 5.000 karakterlik sayfalanmış Markdown içeriğini getirir.
* **KİK (Kamu İhale Kurulu) Araçları:**
* `search_kik_decisions(search_query: KikSearchRequest) -> KikSearchResult`: KİK (Kamu İhale Kurulu) kararlarını arar.
* `get_kik_document_markdown(karar_id: str, page_number: Optional[int] = 1) -> KikDocumentMarkdown`: Belirli bir KİK kararını, Base64 ile encode edilmiş `karar_id`'sini kullanarak alır ve 5.000 karakterlik sayfalanmış Markdown içeriğini getirir.
📜 **Lisans** 📜 **Lisans**
Bu proje MIT Lisansı altında lisanslanmıştır. Detaylar için `LICENSE` dosyasına bakınız. Bu proje MIT Lisansı altında lisanslanmıştır. Detaylar için `LICENSE` dosyasına bakınız.
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env python3
"""Entry point for yargi-mcp package."""
from mcp_server_main import main
if __name__ == "__main__":
main()
+1 -3
View File
@@ -35,8 +35,6 @@ class DanistayApiClient:
headers={ headers={
"Content-Type": "application/json; charset=UTF-8", # Arama endpoint'leri için "Content-Type": "application/json; charset=UTF-8", # Arama endpoint'leri için
"Accept": "application/json, text/plain, */*", # Arama endpoint'leri için "Accept": "application/json, text/plain, */*", # Arama endpoint'leri için
# /getDokuman HTML döndürdüğü için Accept header'ı GET isteğinde farklı olabilir
# ama httpx genellikle bunu yönetir. Gerekirse özel header eklenebilir.
"X-Requested-With": "XMLHttpRequest", "X-Requested-With": "XMLHttpRequest",
}, },
timeout=request_timeout, timeout=request_timeout,
@@ -44,7 +42,7 @@ class DanistayApiClient:
) )
def _prepare_keywords_for_api(self, keywords: List[str]) -> List[str]: def _prepare_keywords_for_api(self, keywords: List[str]) -> List[str]:
return [f'"{k.strip("\"")}"' for k in keywords if k and k.strip()] return ['"' + k.strip('"') + '"' for k in keywords if k and k.strip()]
async def search_keyword_decisions( async def search_keyword_decisions(
self, self,
Regular → Executable
View File
View File
+441
View File
@@ -0,0 +1,441 @@
# kik_mcp_module/client.py
import asyncio
from playwright.async_api import (
async_playwright,
Page,
BrowserContext,
Browser,
Error as PlaywrightError,
TimeoutError as PlaywrightTimeoutError
)
from bs4 import BeautifulSoup
import logging
from typing import Dict, Any, List, Optional
import urllib.parse
import base64 # Base64 için
import re
import html as html_parser
from markitdown import MarkItDown
import os
import math
import tempfile
from .models import (
KikSearchRequest,
KikDecisionEntry,
KikSearchResult,
KikDocumentMarkdown,
KikKararTipi
)
logger = logging.getLogger(__name__)
class KikApiClient:
BASE_URL = "https://ekap.kik.gov.tr"
SEARCH_PAGE_PATH = "/EKAP/Vatandas/kurulkararsorgu.aspx"
FIELD_LOCATORS = {
"karar_tipi_radio_group": "input[name='ctl00$ContentPlaceHolder1$kurulKararTip']",
"karar_no": "input[name='ctl00$ContentPlaceHolder1$txtKararNo']",
"karar_tarihi_baslangic": "input[name='ctl00$ContentPlaceHolder1$etKararTarihBaslangic$EkapTakvimTextBox_etKararTarihBaslangic']",
"karar_tarihi_bitis": "input[name='ctl00$ContentPlaceHolder1$etKararTarihBitis$EkapTakvimTextBox_etKararTarihBitis']",
"resmi_gazete_sayisi": "input[name='ctl00$ContentPlaceHolder1$txtResmiGazeteSayisi']",
"resmi_gazete_tarihi": "input[name='ctl00$ContentPlaceHolder1$etResmiGazeteTarihi$EkapTakvimTextBox_etResmiGazeteTarihi']",
"basvuru_konusu_ihale": "input[name='ctl00$ContentPlaceHolder1$txtBasvuruKonusuIhale']",
"basvuru_sahibi": "input[name='ctl00$ContentPlaceHolder1$txtSikayetci']",
"ihaleyi_yapan_idare": "input[name='ctl00$ContentPlaceHolder1$txtIhaleyiYapanIdare']",
"yil": "select[name='ctl00$ContentPlaceHolder1$ddlYil']",
"karar_metni": "input[name='ctl00$ContentPlaceHolder1$txtKararMetni']",
"search_button_id": "ctl00_ContentPlaceHolder1_btnAra"
}
RESULTS_TABLE_ID = "grdKurulKararSorguSonuc"
NO_RESULTS_MESSAGE_SELECTOR = "div#ctl00_MessageContent1"
VALIDATION_SUMMARY_SELECTOR = "div#ctl00_ValidationSummary1"
MODAL_CLOSE_BUTTON_SELECTOR = "div#detayPopUp.in a#btnKapatPencere_0.close"
DOCUMENT_MARKDOWN_CHUNK_SIZE = 5000
def __init__(self, request_timeout: float = 60000):
self.playwright_instance: Optional[async_playwright] = None
self.browser: Optional[Browser] = None
self.context: Optional[BrowserContext] = None
self.page: Optional[Page] = None
self.request_timeout = request_timeout
self._lock = asyncio.Lock()
async def _ensure_playwright_ready(self, force_new_page: bool = False):
async with self._lock:
browser_recreated = False
context_recreated = False
if not self.playwright_instance:
self.playwright_instance = await async_playwright().start()
if not self.browser or not self.browser.is_connected():
if self.browser: await self.browser.close()
self.browser = await self.playwright_instance.chromium.launch(headless=True)
browser_recreated = True
if not self.context or browser_recreated:
if self.context: await self.context.close()
if not self.browser: raise PlaywrightError("Browser not initialized.")
self.context = await self.browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36",
java_script_enabled=True,
)
context_recreated = True
if not self.page or self.page.is_closed() or force_new_page or context_recreated or browser_recreated:
if self.page and not self.page.is_closed(): await self.page.close()
if not self.context: raise PlaywrightError("Context is None.")
self.page = await self.context.new_page()
if not self.page: raise PlaywrightError("Failed to create new page.")
self.page.set_default_navigation_timeout(self.request_timeout)
self.page.set_default_timeout(self.request_timeout)
if not self.page or self.page.is_closed():
raise PlaywrightError("Playwright page initialization failed.")
logger.debug("_ensure_playwright_ready completed.")
async def close_client_session(self):
async with self._lock:
# ... (öncekiyle aynı)
if self.page and not self.page.is_closed(): await self.page.close(); self.page = None
if self.context: await self.context.close(); self.context = None
if self.browser: await self.browser.close(); self.browser = None
if self.playwright_instance: await self.playwright_instance.stop(); self.playwright_instance = None
logger.info("KikApiClient (Playwright): Resources closed.")
def _parse_decision_entries_from_soup(self, soup: BeautifulSoup, search_karar_tipi: KikKararTipi) -> List[KikDecisionEntry]:
entries: List[KikDecisionEntry] = []
table = soup.find("table", {"id": self.RESULTS_TABLE_ID})
if not table: return entries
rows = table.find_all("tr")
for row_idx, row in enumerate(rows):
if row_idx < 2: continue
cells = row.find_all("td")
if len(cells) == 6:
try:
preview_button_tag = cells[0].find("a", id=re.compile(r"btnOnizle$"))
event_target = ""
if preview_button_tag and preview_button_tag.has_attr('href'):
match = re.search(r"__doPostBack\('([^']*)','([^']*)'\)", preview_button_tag['href'])
if match: event_target = match.group(1)
karar_no_span = cells[1].find("span", id=re.compile(r"lblKno$"))
karar_tarihi_span = cells[2].find("span", id=re.compile(r"lblKtar$"))
idare_span = cells[3].find("span", id=re.compile(r"lblIdare$"))
basvuru_sahibi_span = cells[4].find("span", id=re.compile(r"lblSikayetci$"))
ihale_span = cells[5].find("span", id=re.compile(r"lblIhale$"))
if not (event_target and karar_no_span and karar_tarihi_span): continue
# Karar tipini arama parametresinden alıyoruz, çünkü HTML'de direkt olarak bulunmuyor.
entry = KikDecisionEntry(
preview_event_target=event_target,
kararNo=karar_no_span.get_text(strip=True),
karar_tipi=search_karar_tipi, # Arama yapılan karar tipini ekle
kararTarihi=karar_tarihi_span.get_text(strip=True),
idare=idare_span.get_text(strip=True) if idare_span else None,
basvuruSahibi=basvuru_sahibi_span.get_text(strip=True) if basvuru_sahibi_span else None,
ihaleKonusu=ihale_span.get_text(strip=True) if ihale_span else None,
)
entries.append(entry)
except Exception as e:
logger.error(f"Error parsing a KIK decision entry row: {e}", exc_info=True)
return entries
def _parse_total_records_from_soup(self, soup: BeautifulSoup) -> int:
# ... (öncekiyle aynı) ...
try:
pager_div = soup.find("div", class_="gridToplamSayi")
if pager_div:
match = re.search(r"Toplam Kayıt Sayısı:(\d+)", pager_div.get_text(strip=True))
if match: return int(match.group(1))
except: pass
return 0
def _parse_current_page_from_soup(self, soup: BeautifulSoup) -> int:
# ... (öncekiyle aynı) ...
try:
pager_div = soup.find("div", class_="sayfalama")
if pager_div:
active_page_span = pager_div.find("span", class_="active")
if active_page_span: return int(active_page_span.get_text(strip=True))
except: pass
return 1
async def search_decisions(self, search_params: KikSearchRequest) -> KikSearchResult:
await self._ensure_playwright_ready()
page = self.page
search_url = f"{self.BASE_URL}{self.SEARCH_PAGE_PATH}"
try:
if page.url != search_url:
await page.goto(search_url, wait_until="networkidle", timeout=self.request_timeout)
search_button_selector = f"a[id='{self.FIELD_LOCATORS['search_button_id']}']"
await page.wait_for_selector(search_button_selector, state="visible", timeout=self.request_timeout)
current_karar_tipi_value = search_params.karar_tipi.value
radio_locator_selector = f"{self.FIELD_LOCATORS['karar_tipi_radio_group']}[value='{current_karar_tipi_value}']"
if not await page.locator(radio_locator_selector).is_checked():
js_target_radio = f"ctl00$ContentPlaceHolder1${current_karar_tipi_value}"
async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout):
await page.evaluate(f"javascript:__doPostBack('{js_target_radio}','')")
await page.wait_for_timeout(1000)
async def fill_if_value(selector_key: str, value: Optional[str]):
if value is not None: await page.fill(self.FIELD_LOCATORS[selector_key], value)
# Karar No'yu KİK sitesine göndermeden önce '_' -> '/' dönüşümü yap
karar_no_for_kik_form = None
if search_params.karar_no: # search_params.karar_no Claude'dan '_' ile gelmiş olabilir
karar_no_for_kik_form = search_params.karar_no.replace('_', '/')
logger.info(f"Using karar_no '{karar_no_for_kik_form}' (transformed from '{search_params.karar_no}') for KIK form.")
await fill_if_value('karar_metni', search_params.karar_metni)
await fill_if_value('karar_no', karar_no_for_kik_form) # Dönüştürülmüş halini kullan
# ... (diğer fill_if_value çağrıları aynı) ...
await fill_if_value('karar_tarihi_baslangic', search_params.karar_tarihi_baslangic)
await fill_if_value('karar_tarihi_bitis', search_params.karar_tarihi_bitis)
await fill_if_value('resmi_gazete_sayisi', search_params.resmi_gazete_sayisi)
await fill_if_value('resmi_gazete_tarihi', search_params.resmi_gazete_tarihi)
await fill_if_value('basvuru_konusu_ihale', search_params.basvuru_konusu_ihale)
await fill_if_value('basvuru_sahibi', search_params.basvuru_sahibi)
await fill_if_value('ihaleyi_yapan_idare', search_params.ihaleyi_yapan_idare)
if search_params.yil:
await page.select_option(self.FIELD_LOCATORS['yil'], value=search_params.yil)
action_is_search_button_click = (search_params.page == 1)
event_target_for_submit: str
if action_is_search_button_click:
event_target_for_submit = self.FIELD_LOCATORS['search_button_id']
else: # Pagination
page_link_ctl_number = search_params.page + 2
event_target_for_submit = f"ctl00$ContentPlaceHolder1$grdKurulKararSorguSonuc$ctl14$ctl{page_link_ctl_number:02d}"
try:
async with page.expect_navigation(wait_until="networkidle", timeout=self.request_timeout):
if action_is_search_button_click:
await page.locator(search_button_selector).click()
else:
await page.evaluate(f"javascript:__doPostBack('{event_target_for_submit}','')")
except PlaywrightTimeoutError:
await page.wait_for_timeout(2000)
results_table_dom_selector = f"table#{self.RESULTS_TABLE_ID}"
try:
await page.wait_for_selector(results_table_dom_selector, timeout=30000, state="attached")
await page.wait_for_timeout(2000)
except PlaywrightTimeoutError:
logger.warning(f"Timeout waiting for results table '{results_table_dom_selector}'.")
html_content = await page.content()
soup = BeautifulSoup(html_content, "html.parser")
# ... (hata ve sonuç yok mesajı kontrolü aynı) ...
validation_summary_tag = soup.find("div", id=self.VALIDATION_SUMMARY_SELECTOR.split('[')[0].split(':')[0])
if validation_summary_tag and validation_summary_tag.get_text(strip=True) and \
("display: none" not in validation_summary_tag.get("style", "").lower() if validation_summary_tag.has_attr("style") else True) and \
validation_summary_tag.get_text(strip=True) != "":
return KikSearchResult(decisions=[], total_records=0, current_page=search_params.page)
message_content_div = soup.find("div", id=self.NO_RESULTS_MESSAGE_SELECTOR.split(':')[0])
if message_content_div and "kayıt bulunamamıştır" in message_content_div.get_text(strip=True).lower():
return KikSearchResult(decisions=[], total_records=0, current_page=1)
# _parse_decision_entries_from_soup'a arama yapılan karar_tipi'ni gönder
decisions = self._parse_decision_entries_from_soup(soup, search_params.karar_tipi)
total_records = self._parse_total_records_from_soup(soup)
current_page_from_html = self._parse_current_page_from_soup(soup)
return KikSearchResult(decisions=decisions, total_records=total_records, current_page=current_page_from_html)
except Exception as e:
logger.error(f"Error during KIK decision search: {e}", exc_info=True)
return KikSearchResult(decisions=[], current_page=search_params.page)
def _clean_html_for_markdown(self, html_content: str) -> str:
# ... (öncekiyle aynı) ...
if not html_content: return ""
return html_parser.unescape(html_content)
def _convert_html_to_markdown_internal(self, html_fragment: str) -> Optional[str]:
# ... (öncekiyle aynı) ...
if not html_fragment: return None
cleaned_html = self._clean_html_for_markdown(html_fragment)
markdown_output = None; temp_file_path = None
try:
md_converter = MarkItDown(enable_plugins=True, remove_alt_whitespace=True, keep_underline=True)
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html", encoding="utf-8") as tmp_html_file:
tmp_html_file.write(cleaned_html); temp_file_path = tmp_html_file.name
markdown_output = md_converter.convert(temp_file_path).text_content
if markdown_output: markdown_output = re.sub(r'\n{3,}', '\n\n', markdown_output).strip()
except Exception as e: logger.error(f"MarkItDown conversion error: {e}", exc_info=True)
finally:
if temp_file_path and os.path.exists(temp_file_path): os.remove(temp_file_path)
return markdown_output
async def get_decision_document_as_markdown(
self,
karar_id_b64: str,
page_number: int = 1
) -> KikDocumentMarkdown:
await self._ensure_playwright_ready()
# Bu metodun kendi içinde yeni bir 'page' nesnesi ('doc_page_for_content') kullanacağını unutmayın,
# ana 'self.page' arama sonuçları sayfasında kalır.
current_main_page = self.page # Ana arama sonuçları sayfasını referans alalım
try:
decoded_key = base64.b64decode(karar_id_b64.encode('utf-8')).decode('utf-8')
karar_tipi_value, karar_no_for_search = decoded_key.split('|', 1)
original_karar_tipi = KikKararTipi(karar_tipi_value)
logger.info(f"KIK Get Detail: Decoded karar_id '{karar_id_b64}' to Karar Tipi: {original_karar_tipi.value}, Karar No: {karar_no_for_search}. Requested Markdown Page: {page_number}")
except Exception as e_decode:
logger.error(f"Invalid karar_id format. Could not decode Base64 or split: {karar_id_b64}. Error: {e_decode}")
return KikDocumentMarkdown(retrieved_with_karar_id=karar_id_b64, error_message="Invalid karar_id format.", current_page=page_number)
default_error_response_data = {
"retrieved_with_karar_id": karar_id_b64,
"retrieved_karar_no": karar_no_for_search,
"retrieved_karar_tipi": original_karar_tipi,
"error_message": "An unspecified error occurred.",
"current_page": page_number, "total_pages": 1, "is_paginated": False
}
# Ana arama sayfasında olduğumuzdan emin olalım
if self.SEARCH_PAGE_PATH not in current_main_page.url:
logger.info(f"Not on search page ({current_main_page.url}). Navigating to {self.SEARCH_PAGE_PATH} before targeted search for document.")
await current_main_page.goto(f"{self.BASE_URL}{self.SEARCH_PAGE_PATH}", wait_until="networkidle", timeout=self.request_timeout)
await current_main_page.wait_for_selector(f"a[id='{self.FIELD_LOCATORS['search_button_id']}']", state="visible", timeout=self.request_timeout)
targeted_search_params = KikSearchRequest(
karar_no=karar_no_for_search,
karar_tipi=original_karar_tipi,
page=1
)
logger.info(f"Performing targeted search for Karar No: {karar_no_for_search}")
# search_decisions kendi içinde _ensure_playwright_ready çağırır ve self.page'i kullanır.
# Bu, current_main_page ile aynı olmalı.
search_results = await self.search_decisions(targeted_search_params)
if not search_results.decisions:
default_error_response_data["error_message"] = f"Decision with Karar No '{karar_no_for_search}' (Tipi: {original_karar_tipi.value}) not found by internal search."
return KikDocumentMarkdown(**default_error_response_data)
decision_to_fetch = None
for dec_entry in search_results.decisions:
if dec_entry.karar_no_str == karar_no_for_search and dec_entry.karar_tipi == original_karar_tipi:
decision_to_fetch = dec_entry
break
if not decision_to_fetch:
default_error_response_data["error_message"] = f"Karar No '{karar_no_for_search}' (Tipi: {original_karar_tipi.value}) not present with an exact match in first page of targeted search results."
return KikDocumentMarkdown(**default_error_response_data)
decision_preview_event_target = decision_to_fetch.preview_event_target
logger.info(f"Found target decision. Using preview_event_target: {decision_preview_event_target} for Karar No: {decision_to_fetch.karar_no_str}")
iframe_document_url_str = None
karar_id_param_from_url_on_doc_page = None
document_html_content = ""
try:
logger.info(f"Evaluating __doPostBack on main page to show modal for: {decision_preview_event_target}")
# Bu evaluate, self.page (yani current_main_page) üzerinde çalışır
await current_main_page.evaluate(f"javascript:__doPostBack('{decision_preview_event_target}','')")
await current_main_page.wait_for_timeout(1000)
logger.info(f"Executed __doPostBack for {decision_preview_event_target} on main page.")
iframe_selector = "iframe#iframe_detayPopUp"
modal_visible_selector = "div#detayPopUp.in"
try:
logger.info(f"Waiting for modal '{modal_visible_selector}' to be visible and iframe '{iframe_selector}' src to be populated on main page...")
await current_main_page.wait_for_function(
f"""
() => {{
const modal = document.querySelector('{modal_visible_selector}');
const iframe = document.querySelector('{iframe_selector}');
const modalIsTrulyVisible = modal && (window.getComputedStyle(modal).display !== 'none');
return modalIsTrulyVisible &&
iframe && iframe.getAttribute('src') &&
iframe.getAttribute('src').includes('KurulKararGoster.aspx');
}}
""",
timeout=self.request_timeout / 2
)
iframe_src_value = await current_main_page.locator(iframe_selector).get_attribute("src")
logger.info(f"Iframe src populated: {iframe_src_value}")
except PlaywrightTimeoutError:
logger.warning(f"Timeout waiting for KIK iframe src for {decision_preview_event_target}. Trying to parse from static content after presumed update.")
html_after_postback = await current_main_page.content()
# ... (fallback parsing öncekiyle aynı, default_error_response_data set edilir ve return edilir) ...
soup_after_postback = BeautifulSoup(html_after_postback, "html.parser")
detay_popup_div = soup_after_postback.find("div", {"id": "detayPopUp", "class": re.compile(r"\bin\b")})
if not detay_popup_div: detay_popup_div = soup_after_postback.find("div", {"id": "detayPopUp", "style": re.compile(r"display:\s*block", re.I)})
iframe_tag = detay_popup_div.find("iframe", {"id": "iframe_detayPopUp"}) if detay_popup_div else None
if iframe_tag and iframe_tag.has_attr("src") and iframe_tag["src"]: iframe_src_value = iframe_tag["src"]
else:
default_error_response_data["error_message"]="Timeout or failure finding decision content iframe URL after postback."
return KikDocumentMarkdown(**default_error_response_data)
if not iframe_src_value or not iframe_src_value.strip():
default_error_response_data["error_message"]="Extracted iframe URL for decision content is empty."
return KikDocumentMarkdown(**default_error_response_data)
# iframe_src_value göreceli bir URL ise, ana sayfanın URL'si ile birleştir
iframe_document_url_str = urllib.parse.urljoin(current_main_page.url, iframe_src_value)
logger.info(f"Constructed absolute iframe_document_url_str for goto: {iframe_document_url_str}") # Log this absolute URL
default_error_response_data["source_url"] = iframe_document_url_str
parsed_url = urllib.parse.urlparse(iframe_document_url_str)
query_params = urllib.parse.parse_qs(parsed_url.query)
karar_id_param_from_url_on_doc_page = query_params.get("KararId", [None])[0]
default_error_response_data["karar_id_param_from_url"] = karar_id_param_from_url_on_doc_page
if not karar_id_param_from_url_on_doc_page:
default_error_response_data["error_message"]="KararId (KIK internal ID) not found in extracted iframe URL."
return KikDocumentMarkdown(**default_error_response_data)
logger.info(f"Fetching KIK decision content from iframe URL using a new Playwright page: {iframe_document_url_str}")
doc_page_for_content = await self.context.new_page()
try:
# `goto` metoduna MUTLAK URL verilmeli. Loglanan URL'nin mutlak olduğundan emin olalım.
await doc_page_for_content.goto(iframe_document_url_str, wait_until="domcontentloaded", timeout=self.request_timeout)
document_html_content = await doc_page_for_content.content()
except Exception as e_doc_page:
logger.error(f"Error navigating or getting content from doc_page ({iframe_document_url_str}): {e_doc_page}")
if doc_page_for_content and not doc_page_for_content.is_closed(): await doc_page_for_content.close()
default_error_response_data["error_message"]=f"Failed to load decision detail page: {e_doc_page}"
return KikDocumentMarkdown(**default_error_response_data)
finally:
if doc_page_for_content and not doc_page_for_content.is_closed():
await doc_page_for_content.close()
soup_decision_detail = BeautifulSoup(document_html_content, "html.parser")
karar_content_span = soup_decision_detail.find("span", {"id": "ctl00_ContentPlaceHolder1_lblKarar"})
actual_decision_html = karar_content_span.decode_contents() if karar_content_span else document_html_content
full_markdown_content = self._convert_html_to_markdown_internal(actual_decision_html)
if not full_markdown_content:
default_error_response_data["error_message"]="Markdown conversion failed or returned empty content."
try:
if await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).is_visible(timeout=1000):
await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).click()
except: pass
return KikDocumentMarkdown(**default_error_response_data)
content_length = len(full_markdown_content); total_pages = math.ceil(content_length / self.DOCUMENT_MARKDOWN_CHUNK_SIZE) or 1
current_page_clamped = max(1, min(page_number, total_pages))
start_index = (current_page_clamped - 1) * self.DOCUMENT_MARKDOWN_CHUNK_SIZE
markdown_chunk = full_markdown_content[start_index : start_index + self.DOCUMENT_MARKDOWN_CHUNK_SIZE]
try:
if await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).is_visible(timeout=2000):
await current_main_page.locator(self.MODAL_CLOSE_BUTTON_SELECTOR).click()
await current_main_page.wait_for_selector(f"div#detayPopUp:not(.in)", timeout=5000)
except: pass
return KikDocumentMarkdown(
retrieved_with_karar_id=karar_id_b64,
retrieved_karar_no=karar_no_for_search,
retrieved_karar_tipi=original_karar_tipi,
kararIdParam=karar_id_param_from_url_on_doc_page,
markdown_chunk=markdown_chunk, source_url=iframe_document_url_str,
current_page=current_page_clamped, total_pages=total_pages,
is_paginated=(total_pages > 1), full_content_char_count=content_length
)
except Exception as e:
logger.error(f"Error in get_decision_document_as_markdown for Karar ID {karar_id_b64}: {e}", exc_info=True)
default_error_response_data["error_message"] = f"General error: {str(e)}"
return KikDocumentMarkdown(**default_error_response_data)
+77
View File
@@ -0,0 +1,77 @@
# kik_mcp_module/models.py
from pydantic import BaseModel, Field, HttpUrl, computed_field
from typing import List, Optional
from enum import Enum
import base64 # Base64 encoding/decoding için
class KikKararTipi(str, Enum):
"""Enum for KIK (Public Procurement Authority) Decision Types."""
UYUSMAZLIK = "rbUyusmazlik"
DUZENLEYICI = "rbDuzenleyici"
MAHKEME = "rbMahkeme"
class KikSearchRequest(BaseModel):
"""Model for KIK Decision search criteria."""
karar_tipi: KikKararTipi = Field(KikKararTipi.UYUSMAZLIK, description="Type of KIK Decision.")
karar_no: Optional[str] = Field(None, description="Decision Number (e.g., '2024/UH.II-1766').")
karar_tarihi_baslangic: Optional[str] = Field(None, description="Decision Date Start (DD.MM.YYYY).", pattern=r"^\d{2}\.\d{2}\.\d{4}$")
karar_tarihi_bitis: Optional[str] = Field(None, description="Decision Date End (DD.MM.YYYY).", pattern=r"^\d{2}\.\d{2}\.\d{4}$")
resmi_gazete_sayisi: Optional[str] = Field(None, description="Official Gazette Number.")
resmi_gazete_tarihi: Optional[str] = Field(None, description="Official Gazette Date (DD.MM.YYYY).", pattern=r"^\d{2}\.\d{2}\.\d{4}$")
basvuru_konusu_ihale: Optional[str] = Field(None, description="Tender subject of the application.")
basvuru_sahibi: Optional[str] = Field(None, description="Applicant.")
ihaleyi_yapan_idare: Optional[str] = Field(None, description="Procuring Entity.")
yil: Optional[str] = Field(None, description="Year of the decision.")
karar_metni: Optional[str] = Field(None, description="Keyword/phrase in decision text.")
page: int = Field(1, ge=1, description="Results page number.")
class KikDecisionEntry(BaseModel):
"""Represents a single decision entry from KIK search results."""
preview_event_target: str = Field(..., description="Internal event target for fetching details.")
karar_no_str: str = Field(..., alias="kararNo", description="Raw decision number as extracted from KIK (e.g., '2024/UH.II-1766').")
karar_tipi: KikKararTipi = Field(..., description="The type of decision this entry belongs to.")
karar_tarihi_str: str = Field(..., alias="kararTarihi", description="Decision date.")
idare_str: Optional[str] = Field(None, alias="idare", description="Procuring entity.")
basvuru_sahibi_str: Optional[str] = Field(None, alias="basvuruSahibi", description="Applicant.")
ihale_konusu_str: Optional[str] = Field(None, alias="ihaleKonusu", description="Tender subject.")
@computed_field
@property
def karar_id(self) -> str:
"""
A Base64 encoded unique ID for the decision, combining decision type and number.
Format before encoding: "{karar_tipi.value}|{karar_no_str}"
"""
combined_key = f"{self.karar_tipi.value}|{self.karar_no_str}"
return base64.b64encode(combined_key.encode('utf-8')).decode('utf-8')
class Config:
populate_by_name = True
class KikSearchResult(BaseModel):
"""Model for KIK search results."""
decisions: List[KikDecisionEntry]
total_records: int = 0
current_page: int = 1
class KikDocumentMarkdown(BaseModel):
"""
KIK decision document, with Markdown content potentially paginated.
"""
retrieved_with_karar_id: Optional[str] = Field(None, description="The Base64 encoded karar_id that was used to request this document.")
# Decode edilmiş karar no ve tipini de yanıt olarak ekleyelim, Claude için faydalı olabilir.
retrieved_karar_no: Optional[str] = Field(None, description="The raw KIK Decision Number (e.g., '2024/UH.II-1766') this document pertains to.")
retrieved_karar_tipi: Optional[KikKararTipi] = Field(None, description="The KIK Decision Type this document pertains to.")
karar_id_param_from_url: Optional[str] = Field(None, alias="kararIdParam", description="The KIK system's internal KararId parameter from the document's display URL (KurulKararGoster.aspx).")
markdown_chunk: Optional[str] = Field(None, description="The requested chunk of the decision content converted to Markdown.")
source_url: Optional[str] = Field(None, description="The source URL of the original document (KurulKararGoster.aspx).")
error_message: Optional[str] = Field(None, description="Error message if document retrieval or processing failed.")
current_page: int = Field(1, description="The current page number of the markdown chunk being returned.")
total_pages: int = Field(1, description="The total number of pages the full markdown content is divided into.")
is_paginated: bool = Field(False, description="True if the full markdown content is split into multiple pages.")
full_content_char_count: Optional[int] = Field(None, description="Total character count of the full markdown content before chunking.")
class Config:
populate_by_name = True
+162 -79
View File
@@ -1,11 +1,11 @@
# mcp_server_main.py # mcp_server_main.py
import asyncio import asyncio
import atexit import atexit
import logging import logging
import os import os
from pydantic import HttpUrl from pydantic import HttpUrl, Field
from typing import Optional from typing import Optional, Dict
import urllib.parse # urllib.parse client tarafında kullanılıyor, server'da gerekmeyebilir.
# --- Logging Configuration Start --- # --- Logging Configuration Start ---
LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
@@ -20,7 +20,7 @@ log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(th
file_handler = logging.FileHandler(LOG_FILE_PATH, mode='a', encoding='utf-8') file_handler = logging.FileHandler(LOG_FILE_PATH, mode='a', encoding='utf-8')
file_handler.setFormatter(log_formatter) file_handler.setFormatter(log_formatter)
file_handler.setLevel(logging.DEBUG) file_handler.setLevel(logging.DEBUG)
root_logger.addHandler(file_handler) root_logger.addHandler(file_handler)
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
@@ -32,7 +32,6 @@ logger = logging.getLogger(__name__)
# --- Logging Configuration End --- # --- Logging Configuration End ---
from fastmcp import FastMCP from fastmcp import FastMCP
from pydantic import Field
# --- Module Imports --- # --- Module Imports ---
from yargitay_mcp_module.client import YargitayOfficialApiClient from yargitay_mcp_module.client import YargitayOfficialApiClient
@@ -52,21 +51,30 @@ from uyusmazlik_mcp_module.client import UyusmazlikApiClient
from uyusmazlik_mcp_module.models import ( from uyusmazlik_mcp_module.models import (
UyusmazlikSearchRequest, UyusmazlikSearchResponse, UyusmazlikDocumentMarkdown UyusmazlikSearchRequest, UyusmazlikSearchResponse, UyusmazlikDocumentMarkdown
) )
from anayasa_mcp_module.client import AnayasaMahkemesiApiClient # Norm Denetimi Client from anayasa_mcp_module.client import AnayasaMahkemesiApiClient
from anayasa_mcp_module.bireysel_client import AnayasaBireyselBasvuruApiClient # Bireysel Başvuru Client from anayasa_mcp_module.bireysel_client import AnayasaBireyselBasvuruApiClient
from anayasa_mcp_module.models import ( from anayasa_mcp_module.models import (
AnayasaNormDenetimiSearchRequest, AnayasaNormDenetimiSearchRequest,
AnayasaSearchResult, AnayasaSearchResult,
AnayasaDocumentMarkdown, # For Norm Denetimi documents AnayasaDocumentMarkdown,
AnayasaBireyselReportSearchRequest, # For Bireysel Başvuru reports AnayasaBireyselReportSearchRequest,
AnayasaBireyselReportSearchResult, # For Bireysel Başvuru reports AnayasaBireyselReportSearchResult,
AnayasaBireyselBasvuruDocumentMarkdown, # For Bireysel Başvuru documents AnayasaBireyselBasvuruDocumentMarkdown,
) )
# KIK Module Imports
from kik_mcp_module.client import KikApiClient
from kik_mcp_module.models import (
KikKararTipi,
KikSearchRequest,
KikSearchResult,
KikDocumentMarkdown
)
app = FastMCP( app = FastMCP(
name="TurkishLawResearchAssistantMCP", name="TurkishLawResearchAssistantMCP",
instructions="MCP server for TR legal databases (Yargitay, Danistay, Emsal, Uyusmazlik, Anayasa-Norm, Anayasa-Bireysel).", instructions="MCP server for TR legal databases (Yargitay, Danistay, Emsal, Uyusmazlik, Anayasa-Norm, Anayasa-Bireysel, KIK).",
dependencies=["httpx", "beautifulsoup4", "markitdown", "pydantic", "aiohttp"] dependencies=["httpx", "beautifulsoup4", "markitdown", "pydantic", "aiohttp", "playwright"]
) )
# --- API Client Instances --- # --- API Client Instances ---
@@ -76,8 +84,10 @@ emsal_client_instance = EmsalApiClient()
uyusmazlik_client_instance = UyusmazlikApiClient() uyusmazlik_client_instance = UyusmazlikApiClient()
anayasa_norm_client_instance = AnayasaMahkemesiApiClient() anayasa_norm_client_instance = AnayasaMahkemesiApiClient()
anayasa_bireysel_client_instance = AnayasaBireyselBasvuruApiClient() anayasa_bireysel_client_instance = AnayasaBireyselBasvuruApiClient()
kik_client_instance = KikApiClient()
# --- MCP Tools for Yargitay --- # --- MCP Tools for Yargitay ---
# ... (Yargıtay araçları öncekiyle aynı, docstringler İngilizce) ...
@app.tool() @app.tool()
async def search_yargitay_detailed(search_query: YargitayDetailedSearchRequest) -> CompactYargitaySearchResult: async def search_yargitay_detailed(search_query: YargitayDetailedSearchRequest) -> CompactYargitaySearchResult:
"""Searches Yargitay (Court of Cassation) decisions using detailed criteria.""" """Searches Yargitay (Court of Cassation) decisions using detailed criteria."""
@@ -93,7 +103,7 @@ async def search_yargitay_detailed(search_query: YargitayDetailedSearchRequest)
logger.warning("API response for Yargitay search did not contain expected data structure.") logger.warning("API response for Yargitay search did not contain expected data structure.")
return CompactYargitaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize) return CompactYargitaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_yargitay_detailed' with query: {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_yargitay_detailed'.") # Query loglaması kaldırıldı, PII içerebilir
raise raise
@app.tool() @app.tool()
@@ -104,44 +114,45 @@ async def get_yargitay_document_markdown(document_id: str) -> YargitayDocumentMa
try: try:
return await yargitay_client_instance.get_decision_document_as_markdown(document_id) return await yargitay_client_instance.get_decision_document_as_markdown(document_id)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_yargitay_document_markdown' for ID: {document_id}") logger.exception(f"Error in tool 'get_yargitay_document_markdown'.")
raise raise
# --- MCP Tools for Danistay --- # --- MCP Tools for Danistay ---
# ... (Danıştay araçları öncekiyle aynı, docstringler İngilizce) ...
@app.tool() @app.tool()
async def search_danistay_by_keyword(search_query: DanistayKeywordSearchRequest) -> CompactDanistaySearchResult: async def search_danistay_by_keyword(search_query: DanistayKeywordSearchRequest) -> CompactDanistaySearchResult:
"""Searches Danıştay (Council of State) decisions using keywords.""" """Searches Danıştay (Council of State) decisions using keywords."""
logger.info(f"Tool 'search_danistay_by_keyword' called: {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.info(f"Tool 'search_danistay_by_keyword' called.") # Query loglaması kaldırıldı
try: try:
api_response = await danistay_client_instance.search_keyword_decisions(search_query) api_response = await danistay_client_instance.search_keyword_decisions(search_query)
if api_response.data: if api_response.data:
return CompactDanistaySearchResult( return CompactDanistaySearchResult(
decisions=api_response.data.data, decisions=api_response.data.data,
total_records=api_response.data.recordsTotal, total_records=api_response.data.recordsTotal,
requested_page=search_query.pageNumber, requested_page=search_query.pageNumber,
page_size=search_query.pageSize) page_size=search_query.pageSize)
logger.warning("API response for Danistay keyword search did not contain expected data structure.") logger.warning("API response for Danistay keyword search did not contain expected data structure.")
return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize) return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_danistay_by_keyword': {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_danistay_by_keyword'.")
raise raise
@app.tool() @app.tool()
async def search_danistay_detailed(search_query: DanistayDetailedSearchRequest) -> CompactDanistaySearchResult: async def search_danistay_detailed(search_query: DanistayDetailedSearchRequest) -> CompactDanistaySearchResult:
"""Performs a detailed search for Danıştay (Council of State) decisions.""" """Performs a detailed search for Danıştay (Council of State) decisions."""
logger.info(f"Tool 'search_danistay_detailed' called: {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.info(f"Tool 'search_danistay_detailed' called.") # Query loglaması kaldırıldı
try: try:
api_response = await danistay_client_instance.search_detailed_decisions(search_query) api_response = await danistay_client_instance.search_detailed_decisions(search_query)
if api_response.data: if api_response.data:
return CompactDanistaySearchResult( return CompactDanistaySearchResult(
decisions=api_response.data.data, decisions=api_response.data.data,
total_records=api_response.data.recordsTotal, total_records=api_response.data.recordsTotal,
requested_page=search_query.pageNumber, requested_page=search_query.pageNumber,
page_size=search_query.pageSize) page_size=search_query.pageSize)
logger.warning("API response for Danistay detailed search did not contain expected data structure.") logger.warning("API response for Danistay detailed search did not contain expected data structure.")
return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize) return CompactDanistaySearchResult(decisions=[], total_records=0, requested_page=search_query.pageNumber, page_size=search_query.pageSize)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_danistay_detailed': {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_danistay_detailed'.")
raise raise
@app.tool() @app.tool()
@@ -152,14 +163,15 @@ async def get_danistay_document_markdown(document_id: str) -> DanistayDocumentMa
try: try:
return await danistay_client_instance.get_decision_document_as_markdown(document_id) return await danistay_client_instance.get_decision_document_as_markdown(document_id)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_danistay_document_markdown' for ID: {document_id}") logger.exception(f"Error in tool 'get_danistay_document_markdown'.")
raise raise
# --- MCP Tools for Emsal --- # --- MCP Tools for Emsal ---
# ... (Emsal araçları öncekiyle aynı, docstringler İngilizce) ...
@app.tool() @app.tool()
async def search_emsal_detailed_decisions(search_query: EmsalSearchRequest) -> CompactEmsalSearchResult: async def search_emsal_detailed_decisions(search_query: EmsalSearchRequest) -> CompactEmsalSearchResult:
"""Searches for Emsal (UYAP Precedent) decisions using detailed criteria.""" """Searches for Emsal (UYAP Precedent) decisions using detailed criteria."""
logger.info(f"Tool 'search_emsal_detailed_decisions' called: {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.info(f"Tool 'search_emsal_detailed_decisions' called.") # Query loglaması kaldırıldı
try: try:
api_response = await emsal_client_instance.search_detailed_decisions(search_query) api_response = await emsal_client_instance.search_detailed_decisions(search_query)
if api_response.data: if api_response.data:
@@ -172,7 +184,7 @@ async def search_emsal_detailed_decisions(search_query: EmsalSearchRequest) -> C
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)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_emsal_detailed_decisions': {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_emsal_detailed_decisions'.")
raise raise
@app.tool() @app.tool()
@@ -183,20 +195,21 @@ async def get_emsal_document_markdown(document_id: str) -> EmsalDocumentMarkdown
try: try:
return await emsal_client_instance.get_decision_document_as_markdown(document_id) return await emsal_client_instance.get_decision_document_as_markdown(document_id)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_emsal_document_markdown' for ID: {document_id}") logger.exception(f"Error in tool 'get_emsal_document_markdown'.")
raise raise
# --- MCP Tools for Uyusmazlik --- # --- MCP Tools for Uyusmazlik ---
# ... (Uyuşmazlık araçları öncekiyle aynı, docstringler İngilizce) ...
@app.tool() @app.tool()
async def search_uyusmazlik_decisions( async def search_uyusmazlik_decisions(
search_params: UyusmazlikSearchRequest search_params: UyusmazlikSearchRequest
) -> UyusmazlikSearchResponse: ) -> UyusmazlikSearchResponse:
"""Searches for Uyuşmazlık Mahkemesi decisions using various criteria from the form.""" """Searches for Uyuşmazlık Mahkemesi (Court of Jurisdictional Disputes) decisions using various criteria."""
logger.info(f"Tool 'search_uyusmazlik_decisions' called with params: {search_params.model_dump_json(exclude_none=True, indent=2)}") logger.info(f"Tool 'search_uyusmazlik_decisions' called.") # Query loglaması kaldırıldı
try: try:
return await uyusmazlik_client_instance.search_decisions(search_params) return await uyusmazlik_client_instance.search_decisions(search_params)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_uyusmazlik_decisions': {search_params.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_uyusmazlik_decisions'.")
raise raise
@app.tool() @app.tool()
@@ -212,34 +225,35 @@ async def get_uyusmazlik_document_markdown_from_url(document_url: HttpUrl) -> Uy
try: try:
return await uyusmazlik_client_instance.get_decision_document_as_markdown(str(document_url)) return await uyusmazlik_client_instance.get_decision_document_as_markdown(str(document_url))
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url' for URL: {str(document_url)}") logger.exception(f"Error in tool 'get_uyusmazlik_document_markdown_from_url'.")
raise raise
# --- MCP Tools for Anayasa Mahkemesi (Norm Denetimi) --- # --- MCP Tools for Anayasa Mahkemesi (Norm Denetimi) ---
# ... (Anayasa Norm araçları öncekiyle aynı, docstringler İngilizce) ...
@app.tool() @app.tool()
async def search_anayasa_norm_denetimi_decisions( async def search_anayasa_norm_denetimi_decisions(
search_query: AnayasaNormDenetimiSearchRequest search_query: AnayasaNormDenetimiSearchRequest
) -> AnayasaSearchResult: ) -> AnayasaSearchResult:
""" """
Searches Anayasa Mahkemesi (Constitutional Court) Norm Denetimi decisions Searches Anayasa Mahkemesi (Constitutional Court) Norm Denetimi (Norm Control) decisions
using various criteria from the official search form. This is for https://normkararlarbilgibankasi.anayasa.gov.tr. using criteria from https://normkararlarbilgibankasi.anayasa.gov.tr.
""" """
logger.info(f"Tool 'search_anayasa_norm_denetimi_decisions' called: {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.info(f"Tool 'search_anayasa_norm_denetimi_decisions' called.") # Query loglaması kaldırıldı
try: try:
return await anayasa_norm_client_instance.search_norm_denetimi_decisions(search_query) return await anayasa_norm_client_instance.search_norm_denetimi_decisions(search_query)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_anayasa_norm_denetimi_decisions': {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_anayasa_norm_denetimi_decisions'.")
raise raise
@app.tool() @app.tool()
async def get_anayasa_norm_denetimi_document_markdown( async def get_anayasa_norm_denetimi_document_markdown(
document_url: str = Field(..., description="The URL path of the AYM Norm Denetimi decision (e.g., /ND/YYYY/NN) or full https URL from normkararlarbilgibankasi.anayasa.gov.tr."), document_url: str = Field(..., description="The URL path (e.g., /ND/YYYY/NN) or full https URL of the AYM Norm Denetimi decision from normkararlarbilgibankasi.anayasa.gov.tr."),
page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content, 1-indexed. Default is 1 for the first 5,000 characters.") # Corrected chunk size in description page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed). Default is 1 (first 5,000 characters).")
) -> AnayasaDocumentMarkdown: ) -> AnayasaDocumentMarkdown:
""" """
Retrieves a specific Anayasa Mahkemesi (Norm Denetimi) decision Retrieves a specific Anayasa Mahkemesi (Norm Denetimi) decision
from its URL and returns its content as paginated Markdown. This is for https://normkararlarbilgibankasi.anayasa.gov.tr. from its URL and returns its content as paginated Markdown.
Content is paginated if it exceeds 5,000 characters. Use 'page_number' to get subsequent pages. Content is paginated if it exceeds 5,000 characters. Use 'page_number' for subsequent pages.
""" """
logger.info(f"Tool 'get_anayasa_norm_denetimi_document_markdown' called for URL: {document_url}, Page: {page_number}") logger.info(f"Tool 'get_anayasa_norm_denetimi_document_markdown' called for URL: {document_url}, Page: {page_number}")
if not document_url or not document_url.strip(): if not document_url or not document_url.strip():
@@ -248,10 +262,11 @@ async def get_anayasa_norm_denetimi_document_markdown(
try: try:
return await anayasa_norm_client_instance.get_decision_document_as_markdown(document_url, page_number=current_page_to_fetch) return await anayasa_norm_client_instance.get_decision_document_as_markdown(document_url, page_number=current_page_to_fetch)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_anayasa_norm_denetimi_document_markdown' for URL: {document_url}, Page: {current_page_to_fetch}") logger.exception(f"Error in tool 'get_anayasa_norm_denetimi_document_markdown'.")
raise raise
# --- MCP Tools for Anayasa Mahkemesi (Bireysel Başvuru Karar Raporu & Belgeler) --- # --- MCP Tools for Anayasa Mahkemesi (Bireysel Başvuru Karar Raporu & Belgeler) ---
# ... (Anayasa Bireysel araçları öncekiyle aynı, docstringler İngilizce) ...
@app.tool() @app.tool()
async def search_anayasa_bireysel_basvuru_report( async def search_anayasa_bireysel_basvuru_report(
search_query: AnayasaBireyselReportSearchRequest search_query: AnayasaBireyselReportSearchRequest
@@ -259,79 +274,147 @@ async def search_anayasa_bireysel_basvuru_report(
""" """
Searches Anayasa Mahkemesi (Constitutional Court) Bireysel Başvuru (Individual Application) Searches Anayasa Mahkemesi (Constitutional Court) Bireysel Başvuru (Individual Application)
decisions and generates a 'Karar Arama Raporu' (Decision Search Report). decisions and generates a 'Karar Arama Raporu' (Decision Search Report).
This is for https://kararlarbilgibankasi.anayasa.gov.tr and uses the KararBulteni=1 parameter. This is for https://kararlarbilgibankasi.anayasa.gov.tr (uses KararBulteni=1).
The report typically displays 10 decisions per page by default.
""" """
logger.info(f"Tool 'search_anayasa_bireysel_basvuru_report' called: {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.info(f"Tool 'search_anayasa_bireysel_basvuru_report' called.") # Query loglaması kaldırıldı
try: try:
return await anayasa_bireysel_client_instance.search_bireysel_basvuru_report(search_query) return await anayasa_bireysel_client_instance.search_bireysel_basvuru_report(search_query)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'search_anayasa_bireysel_basvuru_report': {search_query.model_dump_json(exclude_none=True, indent=2)}") logger.exception(f"Error in tool 'search_anayasa_bireysel_basvuru_report'.")
raise raise
@app.tool() @app.tool()
async def get_anayasa_bireysel_basvuru_document_markdown( async def get_anayasa_bireysel_basvuru_document_markdown(
document_url_path: str = Field(..., description="The URL path of the AYM Bireysel Başvuru decision (e.g., /BB/YYYY/NNNN) from kararlarbilgibankasi.anayasa.gov.tr."), document_url_path: str = Field(..., description="The URL path (e.g., /BB/YYYY/NNNN) of the AYM Bireysel Başvuru decision from kararlarbilgibankasi.anayasa.gov.tr."),
page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content, 1-indexed. Default is 1 for the first 5,000 characters.") page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed). Default is 1 (first 5,000 characters).")
) -> AnayasaBireyselBasvuruDocumentMarkdown: ) -> AnayasaBireyselBasvuruDocumentMarkdown:
""" """
Retrieves a specific Anayasa Mahkemesi Bireysel Başvuru (Individual Application) decision Retrieves a specific Anayasa Mahkemesi Bireysel Başvuru (Individual Application) decision
from its URL path (e.g., /BB/YYYY/NNNN found in report results) and returns its content as paginated Markdown. from its URL path (e.g., /BB/YYYY/NNNN) and returns content as paginated Markdown.
This is for https://kararlarbilgibankasi.anayasa.gov.tr. Content is paginated if it exceeds 5,000 characters. Use 'page_number' for subsequent pages.
Content is paginated if it exceeds 5,000 characters. Use 'page_number' to get subsequent pages.
""" """
logger.info(f"Tool 'get_anayasa_bireysel_basvuru_document_markdown' called for URL path: {document_url_path}, Page: {page_number}") logger.info(f"Tool 'get_anayasa_bireysel_basvuru_document_markdown' called for URL path: {document_url_path}, Page: {page_number}")
if not document_url_path or not document_url_path.strip() or not document_url_path.startswith("/BB/"): if not document_url_path or not document_url_path.strip() or not document_url_path.startswith("/BB/"):
raise ValueError("Document URL path (e.g., /BB/YYYY/NNNN) is required for Anayasa Bireysel Başvuru document retrieval.") raise ValueError("Document URL path (e.g., /BB/YYYY/NNNN) is required for Anayasa Bireysel Başvuru document retrieval.")
current_page_to_fetch = page_number if page_number is not None and page_number >= 1 else 1 current_page_to_fetch = page_number if page_number is not None and page_number >= 1 else 1
try: try:
return await anayasa_bireysel_client_instance.get_decision_document_as_markdown(document_url_path, page_number=current_page_to_fetch) return await anayasa_bireysel_client_instance.get_decision_document_as_markdown(document_url_path, page_number=current_page_to_fetch)
except Exception as e: except Exception as e:
logger.exception(f"Error in tool 'get_anayasa_bireysel_basvuru_document_markdown' for URL path: {document_url_path}, Page: {current_page_to_fetch}") logger.exception(f"Error in tool 'get_anayasa_bireysel_basvuru_document_markdown'.")
raise raise
# --- MCP Tools for KIK (Kamu İhale Kurulu) ---
@app.tool()
async def search_kik_decisions(search_query: KikSearchRequest) -> KikSearchResult:
"""
Searches KIK (Public Procurement Authority) decisions.
"""
logger.info(f"Tool 'search_kik_decisions' called.") # Query loglaması kaldırıldı
try:
api_response = await kik_client_instance.search_decisions(search_query)
page_param_for_log = search_query.page if hasattr(search_query, 'page') else 1
if not api_response.decisions and api_response.total_records == 0 and page_param_for_log == 1:
logger.warning(f"KIK search returned no decisions for query.") # Query detayı kaldırıldı
return api_response
except Exception as e:
logger.exception(f"Error in KIK search tool 'search_kik_decisions'.")
current_page_val = search_query.page if hasattr(search_query, 'page') else 1
return KikSearchResult(decisions=[], total_records=0, current_page=current_page_val)
@app.tool()
async def get_kik_document_markdown(
karar_id: str = Field(..., description="The Base64 encoded KIK decision identifier."),
page_number: Optional[int] = Field(1, ge=1, description="Page number for paginated Markdown content (1-indexed). Default is 1.")
) -> KikDocumentMarkdown:
"""
Retrieves a specific KIK (Public Procurement Authority) decision using its Base64 encoded 'karar_id'.
Content is returned as paginated Markdown.
"""
logger.info(f"Tool 'get_kik_document_markdown' called for KIK karar_id: {karar_id}, Markdown Page: {page_number}")
if not karar_id or not karar_id.strip():
logger.error("KIK Document retrieval: karar_id cannot be empty.")
return KikDocumentMarkdown(
retrieved_with_karar_id=karar_id,
error_message="karar_id is required and must be a non-empty string.",
current_page=page_number or 1,
total_pages=1,
is_paginated=False
)
current_page_to_fetch = page_number if page_number is not None and page_number >= 1 else 1
try:
return await kik_client_instance.get_decision_document_as_markdown(
karar_id_b64=karar_id,
page_number=current_page_to_fetch
)
except Exception as e:
logger.exception(f"Error in KIK document retrieval tool 'get_kik_document_markdown' for karar_id: {karar_id}")
return KikDocumentMarkdown(
retrieved_with_karar_id=karar_id,
error_message=f"Tool-level error during KIK document retrieval: {str(e)}",
current_page=current_page_to_fetch,
total_pages=1,
is_paginated=False
)
# --- Application Shutdown Handling --- # --- Application Shutdown Handling ---
# ... (perform_cleanup ve main fonksiyonları öncekiyle aynı, değişiklik yok) ...
def perform_cleanup(): def perform_cleanup():
logger.info("MCP Server performing cleanup...") logger.info("MCP Server performing cleanup...")
try: try:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop_policy().get_event_loop()
if loop.is_closed(): if loop.is_closed():
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
except RuntimeError: # pragma: no cover except RuntimeError:
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
clients_to_close = [
try: globals().get('yargitay_client_instance'),
clients_to_close = [ globals().get('danistay_client_instance'),
globals().get('yargitay_client_instance'), globals().get('emsal_client_instance'),
globals().get('danistay_client_instance'), globals().get('uyusmazlik_client_instance'),
globals().get('emsal_client_instance'), globals().get('anayasa_norm_client_instance'),
globals().get('uyusmazlik_client_instance'), globals().get('anayasa_bireysel_client_instance'),
globals().get('anayasa_norm_client_instance'), globals().get('kik_client_instance')
globals().get('anayasa_bireysel_client_instance') ]
] async def close_all_clients_async():
tasks = []
for client_instance in clients_to_close: for client_instance in clients_to_close:
if client_instance and hasattr(client_instance, 'close_client_session') and callable(client_instance.close_client_session): if client_instance and hasattr(client_instance, 'close_client_session') and callable(client_instance.close_client_session):
logger.info(f"Closing client session for {client_instance.__class__.__name__} via atexit.") logger.info(f"Scheduling close for client session: {client_instance.__class__.__name__}")
loop.run_until_complete(client_instance.close_client_session()) tasks.append(client_instance.close_client_session())
except Exception as e: if tasks:
logger.error(f"Error during atexit cleanup: {e}") results = await asyncio.gather(*tasks, return_exceptions=True)
logger.info("MCP Server atexit cleanup attempt finished.") for i, result in enumerate(results):
if isinstance(result, Exception):
client_name = "Unknown Client"
if i < len(clients_to_close) and clients_to_close[i] is not None:
client_name = clients_to_close[i].__class__.__name__
logger.error(f"Error closing client {client_name}: {result}")
try:
if loop.is_running():
asyncio.ensure_future(close_all_clients_async(), loop=loop)
logger.info("Client cleanup tasks scheduled on running event loop.")
else:
loop.run_until_complete(close_all_clients_async())
logger.info("Client cleanup tasks completed via run_until_complete.")
except Exception as e:
logger.error(f"Error during atexit cleanup execution: {e}", exc_info=True)
logger.info("MCP Server atexit cleanup process finished.")
atexit.register(perform_cleanup) atexit.register(perform_cleanup)
def main():
# --- Main Entry Point --- logger.info(f"Starting {app.name} server via main() function...")
if __name__ == "__main__":
logger.info(f"Starting {app.name} server (main execution block)...")
logger.info(f"Logs will be written to: {LOG_FILE_PATH}") logger.info(f"Logs will be written to: {LOG_FILE_PATH}")
try: try:
asyncio.run(app.run_sse_async(host="127.0.0.1", port=8890, log_level="debug")) app.run()
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Server shut down by user (KeyboardInterrupt).") logger.info("Server shut down by user (KeyboardInterrupt).")
except Exception as e: except Exception as e:
logger.exception("Server failed to start or crashed.") logger.exception("Server failed to start or crashed.")
finally: finally:
logger.info(f"{app.name} server has shut down.") logger.info(f"{app.name} server has shut down.")
if __name__ == "__main__":
main()
+24
View File
@@ -0,0 +1,24 @@
[project]
name = "yargi-mcp"
version = "0.1.0"
description = "MCP Server For Turkish Legal Databases"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"beautifulsoup4>=4.13.4",
"fastmcp>=2.2.10",
"httpx>=0.28.1",
"markitdown>=0.1.1",
"pydantic>=2.11.4",
"aiohttp>=3.11.18",
"playwright>=1.52.0",
]
[project.scripts]
yargi-mcp = "mcp_server_main:main"
[tool.setuptools]
py-modules = ["mcp_server_main"]
[tool.setuptools.packages.find]
include = ["*_mcp_module"]