Mert Özen Her Satırda Daha İleri
Yapay Zeka AI Agent'ları ve MCP — #08

Model Context Protocol Nedir? Host, Client, Server ve 2026-07-28 Spec'i

Mert Özen 8 Kas 2026 10 dk 2 görüntülenme
Model Context Protocol Nedir? Host, Client, Server ve 2026-07-28 Spec'i

MCP, tool ve veri kaynaklarını bir kez server olarak yazıp her AI uygulamasına standart bir client üzerinden bağlamanı sağlayan JSON-RPC tabanlı bir protokoldür. Host, client ve server rollerini, tools/resources/prompts primitive'lerini, stdio ve Streamable HTTP transport'larını ve el sıkışmasını kaldıran 2026-07-28 revizyonunu elle yazılmış JSON mesajlarıyla kabloda görüyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10, MCP (Model Context Protocol) spec 2026-07-28 ve ModelContextProtocol 2.4.0 ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore asistanına sipariş durumunu soran bir tool vermek istiyorsun. Bu yazıda otuz satır civarında bir C# programı yazacağız; program bir MCP server olacak ve tek tool'u get_order_status olacak. Sonra bu programa terminalden, elle yazılmış üç satır JSON (JavaScript Object Notation) göndereceğiz. Soru şu: ilk mesaj olarak bir "merhaba, ben şu sürümü konuşuyorum" el sıkışması göndermezsen server tool listesini verir mi, yoksa önce initialize bekler mi?

Bir yıl önce cevap "önce initialize" olurdu. 2026-07-28 revizyonuyla el sıkışması tamamen kalktı: her istek kendi sürümünü ve yeteneklerini yanında taşıyor, server her isteği bağımsız değerlendiriyor. Bu yazıda protokolün rollerini, mesajlarını ve bu revizyonun neyi değiştirdiğini kabloya bakarak göreceğiz.

N×M Problemi ve MCP'nin Cevabı

Büyük dil modellerini anlatan LLM (Large Language Model) Temelleri serisinin 11. ve 12. yazılarında tool calling'in mekaniğini gördük: model fonksiyon çağırmaz, çağrı ister; çağrıyı senin kodun yapar. Bu serinin ilk yedi yazısında da bu mekaniğin üstüne agent loop'unu, tool tasarımını ve onay akışını kurduk. Hepsinde tool'lar uygulamanın içindeydi: AIFunctionFactory.Create ile bir C# method'unu tool yaptık ve aynı process'te çalıştırdık.

Şimdi tabloyu büyüt. Şirkette üç AI (Artificial Intelligence) uygulaması var: müşteri asistanı, IDE (Integrated Development Environment) içindeki kod asistanı ve iç operasyon botu. Bağlanacakları beş sistem var: sipariş veritabanı, kargo firmasının API'si (Application Programming Interface), stok servisi, doküman deposu, ticket sistemi. Her uygulama her sisteme kendi entegrasyonunu yazarsa 3×5 = 15 ayrı entegrasyon olur ve her yeni uygulama beş yeni entegrasyon demektir. MCP bu N×M problemini N+M'ye indirmek için var: her sistem bir kez MCP server olarak yazılır, her uygulama bir kez MCP client konuşmayı öğrenir.

LLM Temelleri 20. yazıda MCP'nin Kasım 2024'te Anthropic tarafından açık standart olarak önerildiğini gördük. Spec, LSP'den (Language Server Protocol) ilham aldığını açıkça söylüyor: LSP her editörün her dil için ayrı eklenti yazma derdini nasıl çözdüyse MCP de AI uygulamalarının tool ve veri entegrasyonunu öyle standartlaştırıyor.

08-mcp-architecture-tr.png

Spec üç rol tanımlıyor ve bunları karıştırmak ileride tasarım hatasına dönüşüyor:

  • Host: kullanıcının etkileşime girdiği LLM uygulaması. Modeli o çağırır, kullanıcı onayını o ister, hangi tool'un modele gösterileceğine o karar verir.
  • Client: host'un içinde, tek bir server'a bağlı bağlantı nesnesi. C# SDK'sında (Software Development Kit) bu McpClient; üç server'a bağlanan host üç client tutar.
  • Server: tool, resource ve prompt sunan program. Modeli görmez, diğer server'ları görmez; yalnız kendisine gelen istekleri görür.

Bu ayrımın pratik sonucu şu: güvenlik ve onay kararları host'ta verilir. Server "bu tool tehlikeli değil" diyen bir işaret gönderebilir ama onu doğrulayacak olan yine host'tur. 13. yazıda bu noktaya döneceğiz.

Kablodaki Mesajlar: Elle Yazılmış JSON-RPC

Teoriyi bırakıp en küçük server'ı yazalım. 9. yazıda SDK'yı ayrıntılı göreceğiz; şimdilik amaç kabloyu görmek:

using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);

builder.Services
    .AddMcpServer(o => o.ServerInfo = new() { Name = "bookstore-hello", Version = "0.1.0" })
    .WithStdioServerTransport()
    .WithTools<OrderTools>();

await builder.Build().RunAsync();

[McpServerToolType]
public sealed class OrderTools
{
    private static readonly Dictionary<int, string> Orders = new()
    {
        [1043] = "Shipped, tracking TRK-88213",
        [1044] = "Processing",
    };

    [McpServerTool(Name = "get_order_status", ReadOnly = true)]
    [Description("Returns the status of a Bookstore order.")]
    public static string GetOrderStatus([Description("Order number, e.g. 1043")] int orderId) =>
        Orders.TryGetValue(orderId, out string? status)
            ? $"Order {orderId}: {status}"
            : $"Order {orderId} was not found.";
}

Bu server stdio transport'unu kullanıyor: client server'ı bir alt process olarak başlatır, mesajları stdin'e yazar, cevapları stdout'tan okur. Her mesaj tek satırlık bir JSON-RPC (JSON Remote Procedure Call) 2.0 mesajıdır. O zaman client'ı da elle oynayabiliriz:

#!/bin/zsh
# Her istekte aynı _meta: sürüm, client kimliği, client yetenekleri
META='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"probe","version":"0.1"},"io.modelcontextprotocol/clientCapabilities":{}}'
{
  echo '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{'$META'}}'
  echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{'$META'}}'
  echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_order_status","arguments":{"orderId":1043},'$META'}}'
  sleep 2
} | dotnet Hello/bin/Debug/net10.0/Bookstore.Mcp.Hello.dll 2>server.log

İlk satırda initialize yok. Server'ın stdout'a yazdığı üç satırdan birincisi ve üçüncüsü:

{"result":{"supportedVersions":["2026-07-28"],"capabilities":{"tools":{"listChanged":true}},"ttlMs":0,"cacheScope":"private","resultType":"complete","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"bookstore-hello","version":"0.1.0"}}},"id":1,"jsonrpc":"2.0"}
{"result":{"content":[{"type":"text","text":"Order 1043: Shipped, tracking TRK-88213"}],"resultType":"complete","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"bookstore-hello","version":"0.1.0"}}},"id":3,"jsonrpc":"2.0"}

Ortadaki tools/list cevabının tek tool'u da şu:

{"name":"get_order_status","description":"Returns the status of a Bookstore order.","inputSchema":{"type":"object","properties":{"orderId":{"description":"Order number, e.g. 1043","type":"integer"}},"required":["orderId"]},"annotations":{"readOnlyHint":true}}

Bu üç satırda protokolün iskeleti var. server/discover cevabı server'ın hangi sürümleri konuştuğunu, hangi yetenekleri sunduğunu ve kim olduğunu tek seferde söylüyor. tools/list, C# method imzasından üretilmiş bir JSON Schema veriyor; modelin göreceği şey tam olarak bu schema ve description. tools/call da sonucu content blokları olarak döndürüyor. Her cevapta resultType: "complete" var ve server kimliği artık _meta içinde dönüyor.

server.log'a bakınca küçük ama öğretici bir ayrıntı görülüyor:

      Server (bookstore-hello 0.1.0), Client (probe 0.1) method 'tools/list' request handler called.
      Server (bookstore-hello 0.1.0), Client (probe 0.1) method 'tools/call' request handler called.
      Server (bookstore-hello 0.1.0), Client (probe 0.1) method 'server/discover' request handler called.

İstekleri 1, 2, 3 sırasıyla gönderdik ama server onları paralel işledi. Cevapları birbirine bağlayan şey sıra değil, JSON-RPC id'si. Kendi client'ını yazarsan "gönderdiğim sırayla cevap gelir" varsayımı ilk yük altında kırılır.

Primitive'ler: Tools, Resources, Prompts

Server'ın sunduğu üç şeye spec "primitive" diyor ve her birinin kontrolü farklı tarafta:

PrimitiveKim kullanırBookstore örneğiMethod'lar
ToolsModel çağrı ister, host çalıştırırget_order_status, search_bookstools/list, tools/call
ResourcesUygulama ya da kullanıcı seçer, context'e koyarbookstore://books/3resources/list, resources/read
PromptsKullanıcı seçer (ör. slash command)recommend_booksprompts/list, prompts/get

Ayrım önemli çünkü güven modeli farklı. Tool bir eylemdir; kitap aramak masum, siparişi iptal etmek değil. Resource veridir; modelin bağlamına girer ama kendiliğinden bir şey yapmaz. Prompt hazır bir mesaj template'idir; kullanıcı "bana yazılım kitabı öner" komutunu seçer, server mesajı üretir.

Ters yönde, client'ın server'a sunduğu tek aktif özellik elicitation: server, işlemin ortasında kullanıcıdan bilgi ya da onay ister. Roots, sampling ve logging 2026-07-28'de deprecated oldu; çalışmaya devam ediyorlar ama yeni kodda kullanılmamaları isteniyor. Spec bunların yerine şunları öneriyor: dizinleri tool parametresiyle ya da configuration ile vermek, LLM'i server'dan doğrudan çağırmak, log'u stderr'e ya da OpenTelemetry'ye yazmak.

Transport'lar: stdio ve Streamable HTTP

Mesaj biçimi aynı kalıyor, taşıyan kanal değişiyor. İki standart transport var.

stdio'da client server'ı alt process olarak başlatır. Mesajlar satır satır gider, gömülü yeni satır olamaz. Server stdout'a MCP mesajı dışında hiçbir şey yazamaz; log için stderr serbest. Kapanış da basit: client stdin'i kapatır, server çıkar. Logumuzdaki son satırlar tam olarak bunu gösteriyor: transport completed reading messages ve ardından shutting down. Yerel, tek kullanıcılı araçlar için doğal seçim bu; authorization spec'i de stdio için OAuth akışı yerine kimlik bilgilerinin environment'tan alınmasını söylüyor.

Streamable HTTP'de (HyperText Transfer Protocol) server bağımsız bir servis. Tek bir endpoint vardır (genelde /mcp) ve her JSON-RPC isteği ayrı bir POST'tur. Cevap ya düz application/json ya da yalnız o isteğe ait bir SSE (Server-Sent Events) stream'idir; stream'de önce progress bildirimleri, sonra cevap gelir. 2026-07-28 ile GET stream'i ve session header'ı kalktı, buna karşılık her POST'ta MCP-Protocol-Version, Mcp-Method ve gerekiyorsa Mcp-Name header'ları zorunlu oldu. Böylece load balancer ya da gateway body'yi açmadan "bu bir tools/call, tool adı add_to_cart" diyebiliyor. 10. yazıda bunları curl ile tek tek deneyeceğiz.

2026-07-28 Revizyonu Neyi Değiştirdi?

C# SDK'nın 2.0 duyurusu bu revizyonu bugüne kadarki en büyük protokol revizyonu olarak tanımlıyor. Changelog'daki önemli maddeler şunlar:

  • Protocol seviyesindeki session ve Mcp-Session-Id header'ı kalktı (SEP-2567; SEP: Specification Enhancement Proposal). Çağrılar arası state gereken server'lar kendi ürettikleri handle'ı (ör. basketId) normal tool argümanı olarak dolaştırıyor.
  • initialize/notifications/initialized el sıkışması kalktı (SEP-2575). Her istek _meta içinde io.modelcontextprotocol/protocolVersion ve io.modelcontextprotocol/clientCapabilities taşıyor; server da kimliğini cevabın _meta'sında bildiriyor.
  • server/discover eklendi; server'ların bunu uygulaması zorunlu, client'ların çağırması isteğe bağlı.
  • HTTP GET stream'i ve resources/subscribe yerine tek bir subscriptions/listen geldi: client hangi bildirimleri istediğini söyler, server uzun ömürlü bir stream'den yollar.
  • Server'ın client'a kendi isteğini göndermesi yerine MRTR (Multi Round-Trip Requests) geldi (SEP-2322): server resultType: "input_required" ile ne istediğini döndürür, client cevapları ekleyip aynı isteği tekrarlar.
  • Her sonuçta zorunlu resultType field'ı var; eski server'dan gelen ve bu field'ı taşımayan sonuç "complete" sayılır.
  • tools/list gibi liste cevaplarına ttlMs ve cacheScope eklendi; client listeyi ne kadar süre cache'leyebileceğini buradan öğreniyor. Bizim server ttlMs: 0 ve "private" döndürdü, yani "cache'leme".
  • Tasks çekirdekten çıkıp opsiyonel bir extension oldu; roots, sampling ve logging deprecated; HTTP+SSE transport'u ve Dynamic Client Registration da deprecated.

Bu değişikliklerin hepsi aynı yöne bakıyor: her isteğin kendi başına anlamlı olması. Session yoksa iki istek farklı makinelere düşebilir, sticky session gerekmez, server'ın yeniden başlaması yarım kalmış bir el sıkışmasını bozmaz. Bedeli de var: eskiden session'da tuttuğun state'i artık açıkça taşımak zorundasın.

Tarihli Sürümler ve Eski Server'larla Birlikte Yaşamak

MCP sürümleri semver değil, tarih: 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25 ve 2026-07-28. Tarih, o revizyonun yayımlandığı günü gösteriyor ve geriye uyumsuz değişiklik ancak yeni bir tarihle geliyor. Spec ayrıca bir deprecation politikası benimsedi: deprecated bir özellik en az on iki ay spec'te kalıyor.

Peki 2025-11-25 konuşan eski bir client bizim server'a bağlanırsa? Aynı server'a bu kez eski usul el sıkışmasıyla gidelim:

{"result":{"protocolVersion":"2025-11-25","capabilities":{"logging":{},"tools":{"listChanged":true}},"serverInfo":{"name":"bookstore-hello","version":"0.1.0"}},"id":1,"jsonrpc":"2.0"}
{"result":{"content":[{"type":"text","text":"Order 1044: Processing"}]},"id":2,"jsonrpc":"2.0"}

SDK 2.x her iki çağı da konuşan bir "dual-era" server: initialize gelince eski kurallarla devam ediyor, cevaplarda resultType yok, deprecated logging capability'si de geri geliyor. Bilmediği bir sürümü isteyen client'a ise standart hata dönüyor:

{"error":{"code":-32022,"message":"Unsupported protocol version '2027-01-01'.","data":{"supported":["2026-07-28"],"requested":"2027-01-01"}},"id":1,"jsonrpc":"2.0"}

Dikkat edersen supported listesinde yalnız 2026-07-28 var, oysa server eski handshake'i de kabul ediyor. Spec'in uyumluluk kuralı bu yüzden "hata koduna bakarak eski sürüme düş" değil: stdio'da client önce server/discover gönderir; DiscoverResult ya da tanınan modern bir hata gelirse server moderndir, başka bir hata ya da timeout gelirse initialize'a düşer. HTTP'de aynı karar 400 cevabının body'sine bakılarak verilir. C# client'ı bu kararı kendisi veriyor; 12. yazıda client'ın hangi sürümde anlaştığını ekrana yazdıracağız.

Üretimde Dikkat

Birincisi, spec'in hangi revizyonunu hedeflediğini yaz ve test et. Host'ların hepsi aynı anda 2026-07-28'e geçmeyecek; bu yazıyı yazarken denediğim MCP Inspector 2.14.0 CLI (Command-Line Interface) bile bizim server'a initialize ile bağlandı. Server'ının iki çağda da çalıştığını gösteren birer entegrasyon testi, ileride "neden şu IDE'de görünmüyor" sorusunun cevabını baştan verir.

İkincisi, liste cevaplarının cache'lenebilir olması bir sözleşme. ttlMs verirsen client tool listesini o süre boyunca tekrar sormayabilir; tool eklediğin bir deploy'dan sonra eski liste bir süre daha görünür. Spec ayrıca tools/list'in deterministik sırada dönmesini istiyor; sıra her seferinde değişirse hem client cache'i hem de LLM sağlayıcılarının prompt cache'i boşa gider.

Üçüncüsü, serverInfo server'ın kendi beyanı. Spec bunun yalnız gösterim, log ve debug için olduğunu, güvenlik kararında kullanılmaması gerektiğini açıkça yazıyor. "Adı bookstore ise güvenilirdir" diye bir allowlist yazarsan herhangi bir server o adı kullanabilir.

Dördüncüsü, gözlem. Spec 2026-07-28'de _meta içinde traceparent, tracestate ve baggage anahtarlarıyla OpenTelemetry trace context'inin nasıl taşınacağını tanımladı. Host'tan server'a, server'dan arkadaki API'ye uzanan tek bir trace, 14. yazıda agent trajectory'lerini incelerken işimize yarayacak.

Sık Düşülen Tuzaklar

Birincisi, MCP'yi tool calling'in yerine geçen bir şey sanmak. Model hâlâ aynı tool calling mekaniğiyle çağrı istiyor; MCP yalnız tool'un nerede yaşadığını ve nasıl keşfedildiğini standartlaştırıyor. Tek uygulamada, tek process'te yaşayan üç tool için MCP server yazmak çoğu zaman gereksiz bir katman.

İkincisi, eski blog yazılarındaki örnekleri tarihine bakmadan kopyalamak. 2025'te yazılmış bir örnek Mcp-Session-Id bekler, server'dan client'a elicitation/create isteği gönderir, GET stream'i açar. Bunların hiçbiri 2026-07-28'de yok.

Üçüncüsü, stdio'da stdout'u kirletmek. Server stdout'a MCP mesajı dışında tek bir karakter yazarsa client'ın okuduğu satır bozulur. 9. yazıda bunun nasıl bir dakikalık timeout'a dönüştüğünü gerçek çıktısıyla göreceğiz.

Dördüncüsü, host, client ve server'ı karıştırmak. "MCP server'ım kullanıcıdan onay alsın" dediğinde aslında host'tan bir şey istiyorsun; server ancak MRTR ile "onay lazım" diyebilir, onayı kullanıcıya gösteren host'tur.

Küçük Bir Deneme

★☆☆ Probe script'ine dördüncü bir satır ekleyip tools/call ile orderId olarak "abc" gönder. Cevap bir JSON-RPC error mı, yoksa isError: true taşıyan bir tool sonucu mu? Farkın modele ne söylediğini düşün.

★★☆ _meta'dan io.modelcontextprotocol/protocolVersion'ı silip aynı tools/list isteğini gönder, sonra "2025-11-25" yaz. Her iki cevapta resultType, ttlMs ve _meta var mı? Server isteği hangi çağa ait sayıyor?

★★★ Spec'teki stdio uyumluluk kuralını uygulayan küçük bir probe yaz: önce server/discover gönder, iki saniye içinde result ya da -32022 gelmezse initialize'a düş. Bu yazıdaki server'a ve 2025 sürümlü bir SDK ile yazılmış eski bir server'a karşı çalıştır.

Kabloda neyin aktığını artık biliyorsun. Bir sonraki yazıda bu küçük programı gerçek bir Bookstore server'ına çeviriyoruz: ModelContextProtocol SDK 2.x ile dependency injection'dan servis alan tool'lar, structured output, resource ve prompt tanımları ve MCP Inspector ile test.