AI Atlas
Tüm rehberler
🛠️REHBER

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.

MCPServerSDK
KENDİ MCP SUNUCUNU YAZserver.tsimport { Server }server.tool({ name: 'add_task', inputSchema: { … }, handler})// stdio transportstdioAI Asistan(Claude, Cline, Continue…)list_tasks()add_task(...)search_tasks(q)Yerel görev yöneticisi → tek bir sunucu, her host'la 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 --init

package.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:

  1. ListToolsRequestSchema ile hangi tool'ları sunduğunu, parametre şemalarıyla birlikte ilan ediyorsun.
  2. CallToolRequestSchema ile her tool çağrısını işliyorsun.
  3. StdioServerTransport ile sunucuyu protokole bağlıyorsun.

Python ile yazım

Aynı sunucu, Python tarafında.

pip install mcp

tasks_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.server

Test: 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.server

Tool 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.json veya 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'a bin alanı 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