Kendi MCP Sunucunuzu Yazın
Sıfırdan, kendi AI asistanınıza yetenek katan bir MCP sunucusu yazın — yerel görev yöneticisi senaryosu, TypeScript ve Python örnekleri, MCP destekleyen tüm host'larla uyumlu.
Niye kendi MCP sunucunu yazasın?
MCP (Model Context Protocol), bir AI asistanına dış dünyaya dokunma yeteneği vermenin standart yolu olarak hızla yayılıyor. Hazır sunucu kataloğu (filesystem, github, postgres ve daha onlarcası) çoğu yaygın ihtiyacı kapsasa da bazı durumlarda kendi sunucunu yazmak gerekir:
- Kişisel asistan kurarsın: kendi notlarınla, kendi alışkanlıklarınla, kendi kütüphane/lokasyon verinle konuşan bir asistan istiyorsundur.
- Şirket içi sistem: kurum içi CRM, ERP, ürün API'sini asistana açmak istiyorsundur.
- Paketlenmiş açık kaynak araç: topluluğa yararlı bir entegrasyonu MCP olarak dağıtırsın.
- Kontrol katmanı: bir SaaS API'sini ham vermek yerine güvenli bir tool olarak sunarsın.
Bu rehber, sıfırdan, host'tan bağımsız bir MCP sunucusunu uçtan uca yazmayı anlatır. MCP'yi destekleyen herhangi bir asistan (Claude Desktop, Claude Code, Cline, Continue, gelecekte çıkacak başka host'lar) bu sunucuya bağlanır; protokol tek standart.
MCP'nin parçaları
Bir sunucu üç şey sunar:
- Tools: asistanın çağırabileceği fonksiyonlar (örn.
add_task). - Resources: asistanın okuyabileceği veri kaynakları (örn. dosya, kayıt).
- Prompts: sunucunun önerdiği hazır prompt şablonları.
Çoğu pratik senaryoda tools yeter. Bu rehber tool odaklı.
İletişim stdio üzerinden JSON-RPC ile yapılır. SDK bunu görünmez kılıyor; sen sadece yüksek seviyeli arayüzle çalışıyorsun.
Senaryo: kişisel görev yöneticisi
Asistanına "yarın saat 10'a 'rapor hazırla' eklesin", "bu hafta yapılacaklarımı listele", "biten X görevini işaretle" demek istiyorsun. Veriyi bilinen bir bulut servisinde değil, kendi yerel JSON dosyanda tutmak istiyorsun. Verisi yerel, kontrol sende, AI asistan üzerinden konuşulabilir hâle gelmiş bir yapı.
Tool'ların:
list_tasks(status?, due_before?)— görevleri listele.add_task(title, due?, tags?)— yeni görev ekle.complete_task(id)— görevi tamamlandı işaretle.search_tasks(query)— başlığa veya etikete göre ara.
Veri ~/.tasks.json dosyasında, hepsi yerel.
Aynı sunucuyu önce TypeScript, sonra Python ile yazacağız.
TypeScript ile yazım
Projeyi başlat
mkdir tasks-mcp && cd tasks-mcp
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript tsx @types/node
npx tsc --initpackage.json'a:
{
"type": "module",
"scripts": {
"start": "tsx src/index.ts",
"build": "tsc"
}
}Sunucu kodu
src/index.ts:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { homedir } from "node:os";
import { randomUUID } from "node:crypto";
const STORE = join(homedir(), ".tasks.json");
type Task = {
id: string;
title: string;
due?: string; // ISO date
tags?: string[];
done: boolean;
created: string;
};
async function loadTasks(): Promise<Task[]> {
try {
const raw = await readFile(STORE, "utf8");
return JSON.parse(raw);
} catch {
return [];
}
}
async function saveTasks(tasks: Task[]) {
await writeFile(STORE, JSON.stringify(tasks, null, 2), "utf8");
}
const server = new Server(
{ name: "tasks-mcp", version: "0.1.0" },
{ capabilities: { tools: {} } },
);
// 1. Hangi tool'ları sunduğumuzu söyle
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "list_tasks",
description:
"Yapılacaklar listesini döner. Tamamlanmış/açık filtresi ve son tarih filtresi opsiyoneldir.",
inputSchema: {
type: "object",
properties: {
status: { type: "string", enum: ["open", "done", "all"] },
due_before: { type: "string", description: "ISO tarihi; bu tarihten önce due'su olanları getirir" },
},
},
},
{
name: "add_task",
description: "Yeni bir görev ekler. due isteğe bağlı; tags isteğe bağlı, virgülle ayrılmaz, dizi olur.",
inputSchema: {
type: "object",
properties: {
title: { type: "string", description: "Görev başlığı" },
due: { type: "string", description: "Son tarih, ISO formatında" },
tags: {
type: "array",
items: { type: "string" },
description: "Etiket dizisi (örn. ['iş', 'rapor'])",
},
},
required: ["title"],
},
},
{
name: "complete_task",
description: "Bir görevi tamamlandı olarak işaretler.",
inputSchema: {
type: "object",
properties: { id: { type: "string", description: "Görev ID'si" } },
required: ["id"],
},
},
{
name: "search_tasks",
description: "Başlığa veya etikete göre görev arar.",
inputSchema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
},
],
}));
// 2. Çağrıları işle
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args } = req.params as { name: string; arguments: any };
const tasks = await loadTasks();
switch (name) {
case "list_tasks": {
const status = args?.status ?? "open";
const dueBefore = args?.due_before ? new Date(args.due_before) : null;
const filtered = tasks.filter((t) => {
if (status === "open" && t.done) return false;
if (status === "done" && !t.done) return false;
if (dueBefore && t.due && new Date(t.due) >= dueBefore) return false;
return true;
});
return {
content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }],
};
}
case "add_task": {
const task: Task = {
id: randomUUID().slice(0, 8),
title: args.title,
due: args.due,
tags: args.tags,
done: false,
created: new Date().toISOString(),
};
tasks.push(task);
await saveTasks(tasks);
return {
content: [{ type: "text", text: `Eklendi: ${task.id} — ${task.title}` }],
};
}
case "complete_task": {
const t = tasks.find((x) => x.id === args.id);
if (!t) {
return {
content: [{ type: "text", text: `Görev bulunamadı: ${args.id}` }],
isError: true,
};
}
t.done = true;
await saveTasks(tasks);
return {
content: [{ type: "text", text: `Tamamlandı: ${t.title}` }],
};
}
case "search_tasks": {
const q = (args.query as string).toLowerCase();
const hits = tasks.filter(
(t) =>
t.title.toLowerCase().includes(q) ||
(t.tags ?? []).some((tag) => tag.toLowerCase().includes(q)),
);
return {
content: [{ type: "text", text: JSON.stringify(hits, null, 2) }],
};
}
default:
throw new Error(`Bilinmeyen tool: ${name}`);
}
});
// 3. stdio üzerinden bağlan
const transport = new StdioServerTransport();
await server.connect(transport);Üç ana parça:
ListToolsRequestSchemaile hangi tool'ları sunduğunu, parametre şemalarıyla birlikte ilan ediyorsun.CallToolRequestSchemaile her tool çağrısını işliyorsun.StdioServerTransportile sunucuyu protokole bağlıyorsun.
Python ile yazım
Aynı sunucu, Python tarafında.
pip install mcptasks_mcp/server.py:
import json
import os
import uuid
from datetime import datetime
from pathlib import Path
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
STORE = Path.home() / ".tasks.json"
def load_tasks() -> list[dict]:
if not STORE.exists():
return []
return json.loads(STORE.read_text("utf-8"))
def save_tasks(tasks: list[dict]) -> None:
STORE.write_text(json.dumps(tasks, indent=2, ensure_ascii=False), "utf-8")
server = Server("tasks-mcp")
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="list_tasks",
description="Yapılacaklar listesini döner.",
inputSchema={
"type": "object",
"properties": {
"status": {"type": "string", "enum": ["open", "done", "all"]},
"due_before": {"type": "string"},
},
},
),
Tool(
name="add_task",
description="Yeni bir görev ekler.",
inputSchema={
"type": "object",
"properties": {
"title": {"type": "string"},
"due": {"type": "string"},
"tags": {"type": "array", "items": {"type": "string"}},
},
"required": ["title"],
},
),
Tool(
name="complete_task",
description="Görevi tamamlandı olarak işaretler.",
inputSchema={
"type": "object",
"properties": {"id": {"type": "string"}},
"required": ["id"],
},
),
Tool(
name="search_tasks",
description="Başlığa/etikete göre görev arar.",
inputSchema={
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
),
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
tasks = load_tasks()
if name == "list_tasks":
status = arguments.get("status", "open")
due_before = arguments.get("due_before")
out = []
for t in tasks:
if status == "open" and t["done"]:
continue
if status == "done" and not t["done"]:
continue
if due_before and t.get("due") and t["due"] >= due_before:
continue
out.append(t)
return [TextContent(type="text", text=json.dumps(out, indent=2, ensure_ascii=False))]
if name == "add_task":
task = {
"id": uuid.uuid4().hex[:8],
"title": arguments["title"],
"due": arguments.get("due"),
"tags": arguments.get("tags"),
"done": False,
"created": datetime.utcnow().isoformat(),
}
tasks.append(task)
save_tasks(tasks)
return [TextContent(type="text", text=f"Eklendi: {task['id']} — {task['title']}")]
if name == "complete_task":
for t in tasks:
if t["id"] == arguments["id"]:
t["done"] = True
save_tasks(tasks)
return [TextContent(type="text", text=f"Tamamlandı: {t['title']}")]
return [TextContent(type="text", text=f"Görev bulunamadı: {arguments['id']}")]
if name == "search_tasks":
q = arguments["query"].lower()
hits = [
t for t in tasks
if q in t["title"].lower() or any(q in tag.lower() for tag in (t.get("tags") or []))
]
return [TextContent(type="text", text=json.dumps(hits, indent=2, ensure_ascii=False))]
raise ValueError(f"Bilinmeyen tool: {name}")
async def main():
async with stdio_server() as (read, write):
await server.run(read, write, server.create_initialization_options())
if __name__ == "__main__":
import asyncio
asyncio.run(main())Çalıştır:
python -m tasks_mcp.serverTest: MCP Inspector
Sunucuyu host'a bağlamadan önce yerel olarak test etmenin en kolay yolu, MCP'nin resmi Inspector aracıdır. Bir tarayıcı UI'ı açar, tool'ları manuel çağırır, JSON şemalarını gösterir, hataları izler.
npx @modelcontextprotocol/inspector node dist/index.js
# veya Python tarafı için
npx @modelcontextprotocol/inspector python -m tasks_mcp.serverTool listesini doğrula, birkaç görev ekleyip listele, hatalı parametre vererek hata akışını gör. Bu adımı atlamak demek, host'taki ilk denemende sessiz hatalarla boğuşmak demek.
Bir AI asistana bağlama
MCP standart olduğu için herhangi bir MCP-uyumlu host'a bağlanır. Yapılandırma yapısı host'lar arasında küçük farklılıklar gösterse de mantık aynı: host'a "şu komutu çalıştır, bu sunucuyu kullan" demek.
Tipik konfig şöyle görünür:
{
"mcpServers": {
"tasks": {
"command": "node",
"args": ["/full/path/to/tasks-mcp/dist/index.js"]
}
}
}Python tarafı için:
{
"mcpServers": {
"tasks": {
"command": "python",
"args": ["-m", "tasks_mcp.server"]
}
}
}Ortam değişkenleriyle yapılandırma gerekirse env alanı eklenir. Hassas bilgi (API tokenları) .env dosyasında tutulup ${VAR} placeholder'ı ile referanslanır.
Host'a göre dosya yolu değişir:
- Claude Desktop —
~/Library/Application Support/Claude/claude_desktop_config.json(macOS),%APPDATA%\Claude\claude_desktop_config.json(Windows). - Claude Code — proje köküne
.mcp.jsonveya kullanıcı genelinde~/.claude/mcp.json. - Cline / Continue / diğer host'lar — kendi UI'larından MCP server ekleme alanı veya benzeri JSON dosyası.
- Kendi yazdığın asistan — Anthropic, OpenAI, Google gibi LLM API'lerine MCP client kütüphaneleri çağırılarak entegre edilebilir; topluluk SDK'ları mevcut.
Host'u yeniden başlatınca sunucun bağlanır. Asistana "yarın saat 10'a 'rapor hazırla' ekle" dediğinde Claude/diğer host add_task tool'unu çağırır, dosyaya yazılır, geri özet döner.
En sık tuzaklar
Tool açıklaması yetersiz
Asistan bir tool'u "ne zaman çağıracağını" açıklamasından çıkarır. "Görev ekle" yerine "Yeni bir görev ekler. due ISO tarih biçiminde olmalı; tags etiket dizisidir." gibi tam açıklama yaz. Her parametreye de ayrı description koy. Bu küçük detay, "modelin tool'u doğru anda kullanması" ile "modelin onu hiç çağırmaması" arasındaki farktır.
Yetki sınırı çok geniş
Sunucu yerel dosyaya yazıyor olsa bile, gerçek sistemlere bağlanan sunucularda en düşük yetki ilkesi kritik. Read-only token, kapsamlı IAM rolü, API gateway ile path kısıtı — hangisi mümkünse kullan. Asistanın bir hatası canlı sistemde zarara dönüşmesin.
Hataları yutma
fetch/httpx veya disk işlemi exception attığında onu asistana anlamlı bir metin olarak yansıt. Generic 500 yerine: "Görev bulunamadı: ID 12345". Asistan bu metni okur, kullanıcıya doğru özetler. Sessiz çakılan bir tool deneyimi öldürür.
Schema'da required unutmak
Eksik required alanları, asistanın parametreyi atlamasına izin verir; sunucu sonradan patlatır. JSON şemayı sıkı tut: required listesi tam, opsiyonel alanlar açık ve doğru tiplerle.
stdio dışında transport karıştırmak
MCP'nin HTTP/SSE transport'u da var ama çoğu host stdio bekler. Karmaşık altyapı kurmadan önce stdio ile başla; uzak sunucu ihtiyacı belirginleşince transport değiştir.
Eş zamanlı çağrı güvenliği
Asistan tek mesajda birden fazla tool çağırabilir. Sunucunun thread/async güvenli olduğundan emin ol; bir veritabanı bağlantı havuzu kullanıyorsan tek bir bağlantıyı paylaşmak yerine connection-per-call yap. Bizim örneğimizde JSON dosyasına ardışık yazma var; yüksek eşzamanlılıkta dosya kilidi düşünülmeli.
Yayınlama ve dağıtma
Sunucun çalışıyorsa ekip / topluluk için paketleyebilirsin:
- npm/PyPI:
package.json'abinalanı ekle (TypeScript),npx tasks-mcpçalışsın. - README: kurulum ve örnek konfig dahil et.
- awesome-mcp-servers: GitHub'daki topluluk kataloğuna PR aç → keşfedilebilir olsun.
- Private registry: hassas iş içeriyorsa kurum içi npm/PyPI'a yayınla.
Yaygın senaryo fikirleri
Aynı yapıyı farklı veri kaynaklarına uygulayarak çok çeşitli sunucular üretebilirsin:
- Kişisel notlar / wiki: Markdown dosyalarını okumak ve ararken.
- Akıllı ev: ışıkları aç/kapat, sıcaklığı sor, kamera özetle.
- Müzik kütüphanesi: son dinlenenleri listele, çalma listesine ekle.
- Hesap defteri: harcama ekle, kategori bazlı toplam.
- Dijital arşiv: kitap/film/oyun takip listeleri.
- İş özelinde: CRM, ürün analitik, içerik takvimi, ticket sistemi.
İhtiyacın hayalindeki asistanın "şunu yapabilseydi" dediğin her şey, bir MCP sunucusu olarak yazılabilir.
Devamı için
- MCP — protokolün kavramı.
- MCP Server Kataloğu — yazmadan önce hazır var mı bakmak için.
- Claude Skills Rehberi — skill ve MCP nasıl birlikte çalışır.
- Function Calling — MCP'nin altında yatan tool çağırma davranışı.