MCP ve LangGraph: Araç Protokolü ile Ajan Orkestrasyonunu Birbirinden Ayırt Etmek
Model Context Protocol gerçekte ne yapar? LangGraph nereye kadar uzanır? Ve bu ikisini birlikte kullanmak neden mantıklı?
MCP ve LangGraph: Araç Protokolü ile Ajan Orkestrasyonunu Birbirinden Ayırt Etmek
Model Context Protocol gerçekte ne yapar? LangGraph nereye kadar uzanır? Ve bu ikisini birlikte kullanmak neden mantıklı?
LLM ekosistemi hızla büyürken kavramlar da birbirine karışmaya başladı. “MCP agent mı yazdın?” sorusuna verilen cevapların büyük çoğunluğu ya eksik ya da yanlış. Çünkü MCP bir ajan değil; ajanın kullandığı araçların standartlaştırılmış dilidir. LangGraph ise bu araçları ne zaman ve nasıl kullanacağını bilen sistemin iskeletidir.
Bu yazıda bu iki teknolojiyi sıfırdan çalışan bir Türkçe demo üzerinden anlatacağım. Kod var, mimari diyagram var, ama asıl amaç şu soruya net bir cevap vermek: MCP ile LangGraph arasındaki sınır tam olarak nerede?
Önce Kavramları Yerine Oturtmak
Bir uygulamaya “hava durumu sorgula” veya “birim dönüştür” gibi yetenekler kazandırmak istediğinizde aslında iki ayrı problemi çözmeniz gerekiyor:
Problem 1: Bu yetenekler nasıl tanımlanır, şemaları nasıl paylaşılır, bir istemci bunları nasıl çağırır?
Problem 2: Kullanıcının isteğine bakıp hangi yeteneğin devreye gireceğine kim karar verir? Karar akışı nasıl yönetilir?
MCP birinci problemi çözüyor. LangGraph ikinciyi.
MCP = Tool protokolü ve entegrasyon standardı
LangGraph = Ajan akışı ve karar orkestrasyonu
Bu ayrımı net görmeden yazılan sistemlerde MCP gereksiz yere karmaşık kullanılıyor ya da LangGraph sıradan bir “if-else zinciri”ne dönüşüyor.
Projenin Anatomisi
Sistem üç katmandan oluşuyor:
Kullanıcı
│
▼
LangGraph Agent ← karar ve akış
│ route node
│ respond node
▼
MCP Client ← iletişim protokolü
│ initialize
│ tools/list
│ tools/call
▼
MCP Server ← yetenek katmanı
│ get_weather
│ calculate
│ convert_units
│ analyze_text
│ web_search
│ create_learning_roadmap
▼
Dış API'ler ve lokal fonksiyonlar
Her katmanın sorumluluğu net. Hiçbiri birbirinin işine karışmıyor.

Katman 1: Tool’lar — Saf Python Fonksiyonları
En önemli tasarım kararı şu: tool’lar tools.py içinde MCP'den tamamen bağımsız, sıradan Python fonksiyonları olarak yazıldı. MCP bu fonksiyonları sadece standart bir protokol üzerinden dışa açıyor.
Giriş şemaları Pydantic modelleriyle tanımlı:
class WeatherInput(BaseModel):
city: str = Field(..., description="City name, for example Istanbul")
class CalculatorInput(BaseModel):
expression: str = Field(..., description="A safe arithmetic expression")
Hesap makinesi için Python eval() yerine AST tabanlı bir yaklaşım tercih ettim. Gelen ifade parse ediliyor ve yalnızca izin verilen operatörler çalıştırılıyor:
_BINARY_OPERATORS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
}
_ALLOWED_FUNCTIONS = {
"sqrt": math.sqrt,
}
calculate(CalculatorInput(expression="sqrt(144)")) çağrısını doğrudan birim testinden yapabiliyorsunuz; MCP server ayakta olmak zorunda değil. MCP tool'u daha güçlü yapmıyor. Sadece erişilebilir kılıyor.
Katman 2: MCP Server — Protokol Katmanı
mcp_server.py kasıtlı olarak sade tutuldu. FastMCP dekoratörü ile her tool bir kez sarmalandı:
mcp = FastMCP("turkce-mcp-langgraph-demo")
@mcp.tool()
def get_weather(input: WeatherInput):
"""Get current weather for a city."""
return get_weather_impl(input)
@mcp.tool()
def calculate(input: CalculatorInput):
"""Evaluate a safe arithmetic expression."""
return calculate_impl(input)
FastMCP burada iki şey yapıyor: Pydantic modelinden otomatik JSON şeması üretiyor ve her tool’u tools/list + tools/call JSON-RPC endpoint'lerine bağlıyor. Server'ın kendi iş mantığı yok; köprü görevi görüyor.
Katman 3: MCP Client — Handshake ve Discovery
StdioMCPClient sınıfı MCP server'ı bir subprocess olarak başlatıp STDIO üzerinden JSON-RPC mesajları gönderiyor. Protokol üç adımda ilerliyor:
Adım 1: Handshake
def initialize(self) -> Dict[str, Any]:
response = self.send_request(
"initialize",
{
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "turkce-mcp-client", "version": "1.0.0"},
},
)
self.send_notification("initialized")
return response
initialize isteğinden sonra initialized bildirimi gönderiliyor. Bu el sıkışma tamamlanmadan tool discovery çalışmıyor.
Adım 2: Tool Discovery
def list_tools(self) -> List[Dict[str, Any]]:
response = self.send_request("tools/list")
result = response.get("result") or {}
return result.get("tools") or []
Koda sabit yazılmış hiçbir tool bilgisi yok. Hangi tool’ların mevcut olduğu, şemaları ve açıklamaları runtime’da keşfediliyor.
Adım 3: Tool Call
def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
response = self.send_request(
"tools/call",
{"name": name, "arguments": build_tool_call_arguments(arguments)},
)
return response.get("result")
Katman 4: LangGraph Agent — Karar ve Akış
Ajan iki düğümden oluşuyor: route ve respond. State TypedDict ile tanımlandı:
class AgentState(TypedDict, total=False):
msg: str
tools: List[Dict[str, Any]]
selected_tool: Optional[str]
tool_result: Any
result: str
Graph kurulumu:
graph = StateGraph(AgentState)
graph.add_node("route", route_request)
graph.add_node("respond", generate_response)
graph.set_entry_point("route")
graph.add_edge("route", "respond")
graph.add_edge("respond", END)
Route Node: Hangi Tool Çalışacak?
Routing iki katmanlı. Birinci katman deterministik:
def deterministic_route(message: str) -> Optional[Dict[str, Any]]:
if "hava durumu" in lowered:
city = re.sub(r"\bhava\s+durumu\b", "", stripped, ...).strip()
if city:
return {"tool": "get_weather", "parameters": {"city": city}, ...}
if re.search(r"\d\s*[+\-*/]\s*\d", stripped):
expression = re.sub(r"[^0-9+\-*/().\s]", "", stripped).strip()
return {"tool": "calculate", "parameters": {"expression": expression}, ...}
return None
“İstanbul hava durumu” gibi net kalıplar regex ile yakalanıyor, LLM’e gönderilmiyor. Deterministik eşleşme yoksa keşfedilen tool kataloğu modele iletiliyor ve routing kararı JSON formatında isteniyor:
routing_prompt = f"""Kullanici istegini analiz et ve gerekiyorsa bir MCP tool sec.
Kullanici istegi: {state["msg"]}
Kesfedilen MCP tool'lari:
{catalog}
Sadece JSON formatinda cevap ver:
{{
"tool": "tool_adi" veya "none",
"parameters": {{}} veya null,
"reasoning": "kisa gerekce"
}}
"""
Respond Node: Sonucu Türkçe Formatlamak
Tool sonuçları ham döndürülmüyor. format_tool_result belirli tool'lar için deterministik şablonlar üretiyor:
if tool_name == "get_weather" and isinstance(data, dict):
condition = _translate_weather_condition(str(data.get("condition", "Bilinmiyor")))
return (
f"{data['location']} için hava durumu: {condition}. "
f"Sıcaklık {data.get('temperature', 'bilinmiyor')}, "
f"hissedilen {data.get('feels_like', 'bilinmiyor')}."
)
Matematik sonuçları da LLM’e tekrar hesaplatılmıyor. calculate tool'undan dönen sonuç direkt formatlanıyor. Lokal bir model "25 × 4 + 10 ÷ 2 = 105" diyebilir; tool zaten doğru cevabı verdi.
Bu bilinçli bir tasarım tercihi:
LLM karar vermeye yardımcı olur.
Kesin işler tool tarafından yapılır.
Kritik kalıplar kuralla desteklenir.
Güvenlik: Neden eval() Yok?
Gelen ifadeyi doğrudan eval()'a gönderseydiniz kullanıcı __import__('os').system('rm -rf /') gibi bir şey girebilirdi. AST tabanlı yaklaşımda sadece ast.BinOp, ast.Constant ve izin listesindeki fonksiyonlar geçiyor. Başka her şey ValueError fırlatıyor:
raise ValueError("Unsafe or unsupported expression")
Minimal ama yeterli bir güvenlik sınırı.
Testler: Dış Bağımlılık Olmadan
Testler ne Ollama’ya ne wttr.in’e ne de Serper API’sine bağlanıyor. Yalnızca tools.py içindeki saf fonksiyonlar ve client helper'ları test ediliyor:
python -m unittest discover -s tests -v
Bu mümkün çünkü iş mantığı MCP ve LLM katmanlarından izole tutuldu. calculate fonksiyonu doğrudan FastMCP dekoratörüne gömülseydi bağımsız test etmek çok daha zor olurdu.
Kurulum ve Çalıştırma
# Sanal ortam
python -m venv .venv
source .venv/bin/activate # Windows: .\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
# Ollama
ollama pull llama3:latest
ollama serve
# .env
cp .env.example .env
# OLLAMA_MODEL, OLLAMA_BASE_URL, SERPER_API_KEY ekleyin
# Çalıştır
python mcp_client.py
Demo başladığında önce dört örnek soru otomatik işleniyor, ardından SEN: prompt'u açılıyor.
Sonuç: Ne Öğreniyor Bu Proje?
Katmanlar ayrı tutulduğunda şu şeyler netleşiyor:
MCP tool’ları güçlendirmiyor, erişilebilir kılıyor. Tool MCP olmadan da çalışır. MCP sadece onu herhangi bir istemcinin standart şekilde keşfedip çağırabileceği hale getiriyor.
LangGraph akışı yönetiyor, iş mantığını değil. Graph düğümleri “ne yapılacak”ı değil “hangi sırayla ve koşullarda yapılacak”ı tanımlıyor. İş mantığı tool’larda yaşıyor.
Deterministik kural + LLM routing güçlü bir kombinasyon. Her şeyi modele bırakmak ne güvenilir ne de gerekli. Net kalıplar kural tabanlı, muğlak istekler model tabanlı işlenebilir.
Test edilebilirlik mimari bir karar. tools.py'nin bağımsız olması test yazmayı otomatik olarak kolaylaştırdı. Bu sonradan fark edilen bir şey değil, baştan verilen bir karardı.
Sonraki Adımlar
- HTTP transport: STDIO yerine SSE veya WebSocket eklemek, MCP’nin ağ üzerinde nasıl çalıştığını görmek için iyi bir egzersiz.
- Tool gözlemlenebilirliği: Her tool çağrısını ayrı bir logging katmanına taşımak, gerçek sistemlerde darboğazları anlamak için kritik.
- Paralel tool call: Birden fazla tool’u eş zamanlı çalıştırmak LangGraph’ın gerçek gücünü ortaya koyuyor.
- Farklı LLM’ler: Routing kalitesini farklı modellerle kıyaslamak, hangi kararlara LLM dahil etmenin değerli olduğunu somutlaştırıyor.
Kodu incelemek, testleri çalıştırmak veya kendi tool’larınızı eklemek isterseniz repo bağlantısı: https://github.com/m-peker/mcp-langgraph-tr-demo
메타데이터
- post_id
- da19919ba032
- slug
- mcp-ve-langgraph-araç-protokolü-ile-ajan-orkestrasyonunu-birbirinden-ayırt-etmek-da19919ba032
- url
- https://medium.com/@msapeker/mcp-ve-langgraph-ara%C3%A7-protokol%C3%BC-ile-ajan-orkestrasyonunu-birbirinden-ay%C4%B1rt-etmek-da19919ba032
- canonical_url
- https://medium.com/@msapeker/mcp-ve-langgraph-ara%C3%A7-protokol%C3%BC-ile-ajan-orkestrasyonunu-birbirinden-ay%C4%B1rt-etmek-da19919ba032
- author_url
- https://medium.com/@msapeker
- status
- ok
- fetched_at
- 2026-06-11 06:59:45