Aylardır kodu kendimiz yazmıyoruz, yazdırıyoruz. Bu yazıdaki MCP sunucusunu da Claude yazdı. Bizim işimiz neyi yazdıracağımıza karar vermek ve yazılanı denemekti. Sunucu, gurmezin.com’un yayın kuyruğunu Claude’un sorabileceği üç araç olarak açıyor: zamanlanmış yazılar, kuyruğun durumu ve son yayınlanan yazılar. Bu araçlar siteyi değiştiremiyor, yalnızca okuyabiliyor. Denemeler sırasında üç beklenmedik sonuç çıktı. Bildiğimiz kod yeni sürümde hiç çalışmadı. Ortam değişkenleri iki istemcide farklı davrandı. “Sunucuda print() kullanırsanız her şey bozulur” uyarısını ise bugünkü sürümlerle yeniden üretemedik.
Neden MCP?
MCP (Model Context Protocol), bir yapay zekâ modeline dış dünyadan araç vermenin ortak yolu. Modeli çalıştıran program (Claude Code, Claude Desktop ya da başka bir istemci) “istemci” oluyor, araçları sunan küçük program da “sunucu”. İkisi standart bir protokolle konuşuyor. Bu yüzden bir kez yazılan sunucu, MCP destekleyen her istemcide çalışıyor. Ekim sonunda npm’den aldığımız verilere göre MCP’nin resmî SDK paketi ayda yaklaşık 240 milyon kez indiriliyor. Bir yıl önce bu sayı 31 milyondu.
Bizim için somut bir ihtiyaç vardı. Claude ile Otomasyon #4 yazısında yayın kuyruğunu saatte bir denetleyen bir bekçi kurmuştuk. Bekçi masaüstüne bildirim gönderiyordu, ama Claude ile çalışırken kuyruğu sormak için her seferinde uzun bir curl komutu yazdırmamız gerekiyordu. Kuyruğu Claude’un doğrudan sorabileceği bir araç olarak sunmak, hem bu işi kolaylaştıracak hem de MCP’yi gerçek bir işle denememizi sağlayacaktı.
Proje bilgisi Yazarlar: Ahmet Aksoy & Claude · Tarih: 26.10.2026 Ortam: Python 3.12.3, Ubuntu 24.04 ·
mcp2.3.0 (karşılaştırma için 1.30.0) · Claude Code 2.1.310 Örnek kod: proje 49
Tasarım Kararı: Yalnızca Okuma
Sunucunun ilk sürümünü yazdırmadan önce tek bir kural koyduk: sunucuda siteyi değiştiren hiçbir araç olmayacak. Yazı oluşturma, zamanlama, silme yok. Bunun iki nedeni var. Birincisi, modele verdiğiniz her araç, modelin yanlış anlayabileceği bir talimatla tetiklenebilir. Hiç olmayan bir aracı yanlışlıkla çağıramaz. İkincisi, WordPress’e yazı yükleme işi zaten yayın hattımızda ayrı bir araçla, bizim onayımızla yapılıyor.
MCP bu niyeti araç tanımında belirtmenin bir yolunu da sunuyor: read_only_hint=True gibi işaretler. Bunlar yalnızca ipucu. İstemci bu bilgiyi kullanıcıya gösterebilir ya da onay isteyip istemeyeceğine karar verirken kullanabilir. Ama asıl güvence, yazma aracının hiç olmaması.
Sunucunun özü şöyle:
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import ToolAnnotations
mcp = MCPServer("gurmezin-kuyruk")
OKUMA = ToolAnnotations(read_only_hint=True, destructive_hint=False, open_world_hint=True)
@mcp.tool(annotations=OKUMA)
def kuyruk_durumu(hafta: int = 4) -> dict:
"""Yayın kuyruğunu kuralına göre değerlendirir ve önümüzdeki boş yayın günlerini bulur.
Kural: sitede her zaman en az bir zamanlanmış yazı olmalı; yayın günleri
Pazartesi ve Perşembe 10:00. Durum TAMAM, UYARI ya da KRİTİK olur.
hafta: kaç haftalık yayın günü taransın (1-12).
"""
...
if __name__ == "__main__":
mcp.run(transport="stdio")
Burada dikkat edilecek nokta, fonksiyonun belge metninin (docstring) ve tip ipuçlarının modelin gördüğü araç tanımına dönüşmesi. Model bir aracı ne zaman çağıracağına bu metne bakarak karar veriyor. Yani bu metin bir yorum değil, arayüzün ta kendisi. Kuyruğun değerlendirilmesi ise bekçinin kullandığı fonksiyonla yapılıyor. Aynı kuralı iki yerde ayrı ayrı yazmadık.
Birinci Bulgu: Bilinen Kod Çalışmadı
İnternetteki MCP örneklerinin hemen hepsi şöyle başlıyor:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("demo")
Claude’un ilk denemesi de buydu. Paketi kurup çalıştırdığımızda şu hatayı aldık:
ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was
renamed to MCPServer (from mcp.server.mcpserver import MCPServer) and other APIs changed;
see the migration guide ... or pin 'mcp<2' to keep running v1 code.
mcp paketinin 2.0 sürümü 28 Temmuz 2026’da yayımlanmış ve FastMCP sınıfının adı MCPServer olmuş. Bugün pip install mcp yapan herkes 2.x sürümünü alıyor. Bu sürümden önce yazılmış öğreticiler, Stack Overflow cevapları ve modellerin eğitim verisi ise eski adı kullanıyor. Hata mesajı iyi yazılmış: sorunu, çözümü ve geçiş rehberinin adresini söylüyor. Bu sayede düzeltme birkaç dakika sürdü. Ama ders genel: yapay zekâya yazdırdığınız kod, yazıldığı anda güncel olmayabilir. Hızla gelişen bir kütüphanede, modelin bildiği sürüm ile sizin kurduğunuz sürüm farklı olabilir.
İki somut önlem aldık. Bağımlılığı sürüm aralığıyla yazdık (mcp>=2,<3). Kodu yazdırırken de kurulu sürümün belgesine baktırdık. Geçiş rehberinde başka değişiklikler de var: sunucunun çalışma ayarları yapıcıdan run() fonksiyonuna taşınmış, hata yönetiminde ToolError ile protokol hatası ayrılmış.
İkinci Bulgu: Ortam Değişkenleri İstemciye Göre Değişiyor
Sunucu WordPress’e bağlanmak için kimlik bilgilerini ortam değişkenlerinden (WP_URL, WP_USER, WP_APP_PASSWORD) okuyor. Bilgisayarımızda bu değişkenler ~/.bashrc dosyasında tanımlı. Sunucuyu iki farklı istemciyle başlattık ve sunucuya hangi değişkenlerin ulaştığına baktık.
İlk istemci, mcp paketinin kendi Python istemcisiydi. Bu istemci sunucuya varsayılan olarak yalnızca altı değişken geçiriyor: HOME, LOGNAME, PATH, SHELL, TERM, USER. Kaynak kodunda bu listenin adı DEFAULT_INHERITED_ENV_VARS ve fonksiyonun açıklaması “yalnızca devralınması güvenli olan değişkenler” diyor. Yani kimlik bilgileri açıkça verilmezse sunucuya ulaşmıyor.
İkinci istemci Claude Code’du ve farklı davrandı. Ortamı geri yansıtan küçük bir test aracıyla baktığımızda, sunucunun Claude Code’u başlatan kabuğun bütün değişkenlerini aldığını gördük (kabukta 92, sunucuda 92). Bu da sunucunun çalışıp çalışmamasını, Claude’u nereden başlattığınıza bağlı kılıyor:
| Senaryo | Sunucuya ulaşan WP_URL |
Sonuç |
|---|---|---|
Yapılandırmada env yok, Claude değişkenleri olan kabuktan açıldı |
gerçek adres | Çalıştı |
Yapılandırmada env yok, Claude değişkenleri olmayan ortamdan açıldı |
yok | “Ortam değişkeni eksik” hatası |
Yapılandırmada "WP_URL": "${WP_URL}", değişken tanımlı |
gerçek adres | Çalıştı |
Yapılandırmada "WP_URL": "${WP_URL}", değişken tanımsız |
'${WP_URL}' (metnin kendisi) |
Belirsiz hata |
Son satır bizim kodumuzdaki bir açığı ortaya çıkardı. Claude Code, yapılandırmadaki tanımsız bir ${WP_URL} ifadesini boş bırakmıyor, metni olduğu gibi geçiriyor. Claude’un yazdığı ilk sürüm yalnızca değişkenin boş olup olmadığına bakıyordu. ${WP_URL} metni boş olmadığı için kontrol geçti, istek geçersiz bir adrese gitti ve model bize yalnızca “Error executing tool kuyruk_durumu” diyebildi. Düzeltme tek satır:
eksik = [k for k in ("WP_URL", "WP_USER", "WP_APP_PASSWORD")
if not os.environ.get(k) or os.environ[k].startswith("${")]
Düzeltmeden sonra aynı senaryoda model şu mesajı aktardı: “Ortam değişkeni eksik ya da açılmamış: WP_URL, WP_USER, WP_APP_PASSWORD.” Hata mesajının açık olması burada önemli, çünkü bu mesajı ilk okuyan siz değil, model. Model mesajı anlarsa size doğru şeyi söyler.
Önerimiz: kimlik bilgilerini yapılandırmaya açıkça ${...} ile yazın ve sunucunun içinde gelen değerleri kontrol edin. Böylece sunucu, Claude’u hangi pencereden ya da uygulamadan başlattığınıza bağlı olmaz. Kimlik bilgilerinin kendisi de yapılandırma dosyasına yazılmamış olur.
Üçüncü Bulgu: print() Uyarısı Artık Eskisi Gibi Değil
MCP öğreticilerinin neredeyse hepsinde aynı uyarı var: stdio ile çalışan bir sunucuda print() kullanmayın. Nedeni şu: istemci ile sunucu, standart giriş ve çıkış üzerinden JSON mesajlarıyla konuşuyor. Sunucu stdout’a fazladan bir satır yazarsa bu satır protokolün içine karışır. Uyarı doğru bir mantığa dayanıyor. Biz de bunu ölçmek istedik.
Tek araçlı küçük bir sunucuya print() satırı ekledik ve aracı iki kez çağırdık. Bunu üç biçimde, mcp paketinin iki sürümüyle denedik:
| Sunucudaki satır | mcp 1.30.0 | mcp 2.3.0 |
|---|---|---|
print("...") |
Çıktı hiçbir yerde görünmedi | Sunucu kapanırken protokol kanalına sızdı, istemci “parse edilemedi” kaydı yazdı |
print("...", flush=True) |
Her çağrıda protokol kanalına girdi, istemci “parse edilemedi” kaydı yazdı | stderr’e yönlendirildi |
print("...", file=sys.stderr) |
stderr | stderr |
İki şaşırtıcı sonuç var. Birincisi, mcp 2.x sunucusu çalışırken stdout’u kendisi stderr’e yönlendiriyor. Kaynak kodundaki açıklamaya göre bunun amacı, araç kodunun “kaçak çıktısının protokol kanalını ıskalaması”. İkincisi, protokol kanalına gerçekten çöp satır girdiğinde bile (1.x ve flush=True) ne Python istemcisi ne de Claude Code bozuldu. İkisi de çözemediği satırı atıp çalışmaya devam etti ve iki çağrının sonucunu da doğru döndürdü.
Yani “print() her şeyi bozar” uyarısını bugünkü sürümlerle yeniden üretemedik. Bu, uyarının yanlış olduğu anlamına gelmiyor. Biz yalnızca iki istemciyi denedik, başka istemciler (Claude Desktop, IDE eklentileri, başka şirketlerin araçları) bu satırlara farklı tepki verebilir. Ayrıca 2.x’te satır sonu olmayan bir print(..., end="") çıktısı hiçbir yere ulaşmadan kayboldu; bu da bir hata ayıklama mesajını aradığınızda bulamamanız demek. Kuralımız değişmedi: tanılama mesajlarını sys.stderr‘e ya da logging modülüne yazın. Değişen şey, nedenin “kesin çöker”den “nereye gideceği tampona, sürüme ve istemciye bağlı”ya dönüşmesi. Bu ikincisi daha sinsi, çünkü hatayı görmezsiniz.
Claude Code’da Kullanım
Sunucuyu bir projeye eklemek için proje klasörüne .mcp.json adlı bir dosya koymak yetiyor:
{
"mcpServers": {
"gurmezin": {
"command": "/yol/.venv-mcp/bin/python",
"args": ["/yol/kuyruk_mcp.py"],
"env": {
"WP_URL": "${WP_URL}",
"WP_USER": "${WP_USER}",
"WP_APP_PASSWORD": "${WP_APP_PASSWORD}"
}
}
}
}
Sonra Claude’a doğal bir soru sorduk: “Bir sonraki gurmezin yazısını hangi güne hazırlamalıyım? Kuyrukta neler var?” Claude önce kuyruk_durumu, sonra yayin_kuyrugu aracını çağırdı ve şöyle cevap verdi:
Bir sonraki yazıyı 9 Kasım 2026 Pazartesi, 10:00'a hazırlamalısın. Kuyruktaki ilk boş yayın günü bu.
Kuyrukta zamanlanmış 3 yazı var, durum TAMAM: [29.10, 02.11, 05.11 yazılarının tablosu]
Önümüzdeki boş günler: 9.11 Pzt, 12.11 Per, 16.11 Pzt, 19.11 Per, 23.11 Pzt.
5 Kasım'daki yazı yayımlandıktan sonra yeni bir yazı zamanlanmamışsa kuyruk boşalır. "Her zaman en az
bir zamanlanmış yazı olmalı" kuralını korumak için 9 Kasım yazısını 5 Kasım'dan önce zamanlaman gerekiyor.
Son paragrafı biz istemedik. Model bunu aracın belge metnindeki kuraldan çıkardı. Bu, belge metninin arayüz olduğunun iyi bir örneği.
Sonra salt-okuma tasarımını sınadık: “9 Kasım Pazartesi 10:00 için ‘Deneme’ başlıklı boş bir yazı zamanla.” Model kuyruğa baktı ve şunu söyledi: “Bunu yapamadım. gurmezin MCP sunucusunda yazı oluşturan ya da zamanlayan bir araç yok. Elimdeki üç araç da yalnızca okuma yapıyor.” Ardından bize iki yol önerdi: yazıyı WordPress panelinden elle zamanlamak ya da sunucuya yazma yetkisi olan bir araç eklemek. İkincisini yapmadık.
Çıkardığımız Dersler
- Yazdırılan kod, yazıldığı anda eskimiş olabilir. Hızlı değişen bir kütüphanede bağımlılığı sürüm aralığıyla yazın, hata mesajını okuyun ve modele kurulu sürümün belgesini gösterin.
- Araç tanımı bir arayüzdür. Belge metni ve tip ipuçları modelin gördüğü tek şey. Kuralları oraya yazarsanız model onları uygular.
- Modele verilmeyen araç, yanlış kullanılamaz. Salt-okuma işaretleri ipucudur, asıl güvence yazma aracının olmamasıdır.
- Hata mesajları modele yazılır. “Error executing tool” ile “ortam değişkeni açılmamış” arasındaki fark, modelin size doğru şeyi söyleyip söyleyememesi demek.
- Aynı sunucu, farklı istemcide farklı çalışabilir. Ortam değişkenleri ve stdout davranışı istemciye ve sürüme göre değişiyor. Kendi istemcinizde deneyin.
- Yaygın uyarıları da sınayın. “print() her şeyi bozar” uyarısının bugünkü hali, ezberlenmiş halinden farklı çıktı.
Kodu Kendiniz Deneyin
Proje 49, aynı sunucunun herhangi bir WordPress sitesiyle çalışan genel bir sürümü. WordPress’iniz yoksa örnek veriyle de deneyebilirsiniz:
pip install "mcp>=2,<3"
python main.py --check # örnek veriyle sunucuyu başlatır, üç aracı da çağırır
python main.py --demo # örnek veriyle stdio sunucusu (Claude Code'a bağlamak için)
python main.py # gerçek site: WP_URL, WP_USER, WP_APP_PASSWORD gerekir
--check komutu, sunucuyu bir MCP istemcisi gibi kendisi başlatıyor ve araçların listesini ve çıktılarını yazdırıyor. Bu yazıdaki ortam değişkeni ve print() denemelerinin betikleri de yazının klasöründe duruyor.
Kaynaklar
- Model Context Protocol belgeleri
- MCP Python SDK: v1’den v2’ye geçiş rehberi
- mcp — PyPI: sürüm geçmişi
- Claude Code: MCP sunucuları
- Claude ile Otomasyon #4: Yayın Kuyruğuna Bekçi Koyduk, Bekçi de Bir Kez Sessizce Çöktü
Ahmet Aksoy & Claude
Not: Bu yazıda anlatılan sunucunun genel sürümünü https://github.com/ahmetax/practical-python-examples deposundaki v312/49_mcp_queue_server klasöründe bulabilirsiniz.



