Ömer
Özbay

Full-Stack Engineer

LOADING
©2026
Gerçek Bir AI Agent Projesinde MCP Kullanırken Öğrendiklerim
YAPAY ZEKAMCPAI Agent

Gerçek Bir AI Agent Projesinde MCP Kullanırken Öğrendiklerim

calendar_today13 TEM 2026
schedule7 DK OKUMA
boltİLERİ SEVİYE

Gerçek Bir AI Agent Projesinde MCP Kullanırken Öğrendiklerim

Model Context Protocol (MCP) için resmi dokümantasyon temiz ve basit görünüyor. Bir tutorial'da da öyle. Ama gerçek bir iş akışına — her gün çalışan, production verisine dokunan ve birden fazla harici servisle entegre olan — MCP tabanlı bir agent pipeline'ı deploy ettiğinizde, çok farklı bir gerçeklikle karşılaşıyorsunuz.

Bu bir tutorial değil. Bu, production'da bir MCP agent sistemi kurma ve çalıştırmadan çıkan zor derslerin post-mortem'i. Yaptığım hataları, doğru yaptığım mimari kararları ve başlamadan önce kimsenin söylemediği şeyleri ele alacağım.


Ne İnşa Ediyorduk?

Proje, gucluyumhe.dev için dahili bir otomasyon pipeline'ıydı — Claude tabanlı bir AI agent'ının şunları yapmasına izin veren bir MCP sunucusu:

  1. Log'lardan site performans verilerini okuma ve analiz etme
  2. Yeni blog yazısı taslakları hazırlama ve içerik dizinine kaydetme
  3. Yayınlanan yazıların yerel veritabanını tekrar kontrol için sorgulama
  4. Bir shell komutu aracılığıyla build önizlemelerini tetikleme

Teoride basit. Production'da şaşırtıcı derecede karmaşık.


Ders 1: Araç Tasarımı Her Şeydir — Ve Zordur

En yaygın MCP tutorial hatası çok geniş araçlar tasarlamaktır. İşte kaçınılması gerekenin bir örneği:

// COK GENIS — production'da tehlikeli
{
  name: "execute_command",
  description: "Execute any shell command on the local machine",
  inputSchema: {
    type: "object",
    properties: {
      command: { type: "string", description: "The shell command to run" }
    },
    required: ["command"]
  }
}

Bu neden tehlikeli? Çünkü LLM onu tam tasarlandığı gibi kullanacak. Bir agent ayrıştıramadığı bir dosyayla karşılaşırsa, geçerli shell çözümleri olduğu için yıkıcı komutlar deneyecek. Ve önce sizden izin istemeyecek.

Çözüm: Dar, Amaca Özel Araçlar

// DOGRU: spesifik, güvenli, niyetli
{
  name: "save_blog_draft",
  description: "Yeni bir blog yazısı taslağını content/posts dizinine kaydet. Yalnızca geçerli frontmatter ile markdown içeriği kabul eder.",
  inputSchema: {
    type: "object",
    properties: {
      slug: {
        type: "string",
        pattern: "^[a-z0-9-]+$",
        description: "Dosya adı için URL güvenli slug"
      },
      content: {
        type: "string",
        description: "Frontmatter dahil tam markdown içeriği"
      },
      locale: {
        type: "string",
        enum: ["en", "tr"],
        description: "Blog yazısının dili"
      }
    },
    required: ["slug", "content", "locale"]
  }
}

Pratik kural: bir araç tam olarak bir şey yapmalı ve şeması geçersiz girdileri yapısal olarak imkânsız kılmalıdır.


Ders 2: Araçlarda Hata Yönetimi Sizin Sorumluluğunuz

Bir araç işlenmemiş bir hata fırlattığında, MCP ham bir exception'ı LLM'e iletir. Bazı durumlarda model bunu izin sorunu olarak yorumlar ve eskalasyona gider — daha agresif yaklaşımlar dener.

Bizi Kurtaran Pattern

Her araç işleyicisi fırlatmak yerine yapılandırılmış bir hata yanıtı döndürmelidir:

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    switch (name) {
      case 'save_blog_draft':
        return await handleSaveBlogDraft(args);
      case 'get_post_list':
        return await handleGetPostList(args);
      default:
        return {
          content: [{
            type: "text",
            text: JSON.stringify({
              error: "UNKNOWN_TOOL",
              message: `'${name}' aracı bu sunucuda kayıtlı değil.`,
              availableTools: ['save_blog_draft', 'get_post_list']
            })
          }],
          isError: true
        };
    }
  } catch (err) {
    const error = err instanceof Error ? err : new Error(String(err));
    console.error(`[MCP Araç Hatası] ${name}:`, error.message);

    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          error: "TOOL_EXECUTION_FAILED",
          tool: name,
          message: error.message,
          hint: "İşlem tamamlanamadı. Lütfen giriş parametrelerini doğrulayın."
        })
      }],
      isError: true
    };
  }
});

isError: true bayrağı, LLM'e aracın başarısız olduğunu söyler. Bu da sessizce devam etmek yerine yeniden deneme veya geri dönüş mantığını tetikler.


Ders 3: LLM Düşünmediğiniz Araçları Kötüye Kullanacak

Test sırasında agent'a blog yazılarını okumak için bir read_file aracı verdim. Tek bir oturumda:

  1. Posts dizinindeki tüm markdown dosyalarını okudu — beklenen, doğru
  2. Proje yapısını anlamak için package.json okudu — beklenmedik, ama zararsız
  3. Proje konfigürasyonunu anlamaya çalıştığı için .env okudu — tehlikeli, güvenlik riski

LLM kötü niyetli değil. Gerçekten yardımcı olmaya çalışıyor. Ama bu bağlamda yardımcı olmak, hiç amaçlamadığınız dosyaları okumak anlamına geliyordu.

Çözüm: Katı Yol İzin Listesi

const ALLOWED_DIRECTORIES = [
  '/content/posts',
  '/public/images'
] as const;

async function handleReadFile(args: unknown): Promise<ToolResult> {
  const { path: filePath } = args as { path: string };

  const resolvedPath = path.resolve(PROJECT_ROOT, filePath);

  const isAllowed = ALLOWED_DIRECTORIES.some(dir => {
    const allowedResolved = path.resolve(PROJECT_ROOT, dir);
    return resolvedPath.startsWith(allowedResolved + path.sep) ||
           resolvedPath === allowedResolved;
  });

  if (!isAllowed) {
    return {
      content: [{
        type: "text",
        text: JSON.stringify({
          error: "ACCESS_DENIED",
          message: "Bu dosya yoluna erişilemiyor. Yalnızca /content/posts ve /public/images dizinleri okunabilir."
        })
      }],
      isError: true
    };
  }

  try {
    const content = await fs.readFile(resolvedPath, 'utf-8');
    return { content: [{ type: "text", text: content }] };
  } catch (err) {
    const error = err instanceof Error ? err : new Error(String(err));
    return {
      content: [{ type: "text", text: JSON.stringify({ error: "FILE_READ_FAILED", message: error.message }) }],
      isError: true
    };
  }
}

Ders 4: Production'da Rate Limiting Zorunludur

Rate limiting olmadan, tek bir hatalı davranışlı agent oturumu sistem kaynaklarınızı tüketebilir. Bir test oturumunda agent yeniden deneme döngüsüne girdiğinde, get_post_list'i 90 saniyede 47 kez çağırdı.

Basit Token Bucket Uygulaması

interface RateLimitEntry {
  tokens: number;
  lastRefill: number;
}

const rateLimits = new Map<string, RateLimitEntry>();

function checkRateLimit(toolName: string, maxTokens = 10, refillRateMs = 60000): boolean {
  const now = Date.now();
  const entry = rateLimits.get(toolName);

  if (!entry) {
    rateLimits.set(toolName, { tokens: maxTokens - 1, lastRefill: now });
    return true;
  }

  const elapsed = now - entry.lastRefill;
  const refillAmount = Math.floor(elapsed / refillRateMs) * maxTokens;

  if (refillAmount > 0) {
    entry.tokens = Math.min(maxTokens, entry.tokens + refillAmount);
    entry.lastRefill = now;
  }

  if (entry.tokens <= 0) {
    return false;
  }

  entry.tokens--;
  return true;
}

async function handleGetPostList(args: unknown): Promise<ToolResult> {
  if (!checkRateLimit('get_post_list', 20, 60000)) {
    return {
      content: [{
        type: "text",
        text: JSON.stringify({ error: "RATE_LIMITED", message: "Çok fazla istek. Lütfen bu aracı tekrar çağırmadan önce bekleyin." })
      }],
      isError: true
    };
  }

  // gerçek uygulama devam eder
}

Ders 5: Resource ve Tool Ayrımı Önemlidir

MCP, veri açığa çıkarmak için iki primitive'e sahip: Resources (salt okunur veri) ve Tools (çalıştırılabilir eylemler). Çoğu tutorial yalnızca Tools kullanır çünkü gösterimde daha basittir. Ama okuma ve yazma işlemlerini tek bir primitive'e karıştırmak tasarım sorunlarına yol açar.

Doğru model:

  • Resources: Blog yazısı listesi, site metrikleri, yayınlanan içerik kataloğu — Resource olarak açığa çıkarın
  • Tools: Taslak yaz, build tetikle, bildirim gönder — Tool olarak açığa çıkarın
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri: "content://posts/list",
      name: "Yayınlanan Blog Yazıları",
      description: "Tüm yayınlanan blog yazılarının metadata ile tam listesi",
      mimeType: "application/json"
    }
  ]
}));

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  if (uri === "content://posts/list") {
    const posts = await getAllPosts();
    return {
      contents: [{
        uri,
        mimeType: "application/json",
        text: JSON.stringify(posts, null, 2)
      }]
    };
  }

  throw new Error(`Kaynak bulunamadı: ${uri}`);
});

Bu ayrım aynı zamanda güvenlik açısından da yardımcı olur: tüm yazma işlemlerini (Tools) okuma işlemlerinden (Resources) ayrı olarak denetleyebilirsiniz.


Gerçekten İşe Yarayan Şeyler

Tüm zor derslerden sonra production mimarisi:

Claude Desktop / Agent Host
        |
  MCP Server (TypeScript)
        |
  Izin Listeli Resources (salt okunur)
    content://posts/list
    metrics://pagespeed/latest
        |
  Rate-Limited Tools (yazma islemleri)
    save_blog_draft
    trigger_build_preview
        |
  Yapilandirilmis Hata Yanitlari (her zaman donduruluyor)

Kararlı yapan temel tasarım kararları:

  1. Dar araçlar — her araç bir şey yapıyor
  2. İzin listeli yollar — LLM hassas dosyalara erişemiyor
  3. Yapılandırılmış hatalar — LLM her zaman ayrıştırılabilir yanıt alıyor
  4. Rate limiting — kontrolden çıkan döngüleri önlüyor
  5. Resource ve Tool ayrımı — okuma ve yazma mimari olarak zorunlu kılındı
  6. Denetim loglama — her araç çağrısı argümanlar ve sonuçlarla loglanıyor

Sonuç

MCP gerçekten güçlü. Bir LLM'e yerel ortamınıza ve production sistemlerinize güvenli, yapılandırılmış erişim verme yeteneği, daha önce pratik olmayan otomasyon olanaklarını açıyor.

Ama güvenli varsayılan değil — kasıtlı olarak tasarladığınız bir şey. LLM size verdiğiniz her yeteneği, her zaman beklediğiniz şekillerde kullanacak. Mimar olarak işiniz, yapabileceği şeyin tam olarak istediğiniz şey olmasını ve bundan fazlasını olmamasını sağlamak.

Dar araçlar tasarlayın. Katı sınırlar uygulayın. Yapılandırılmış hatalar döndürün. Her şeyi rate limit'leyin. O zaman sihir gerçekten işe yarar.

Ömer Özbay
YAZAN

Ömer Özbay

Yüksek performanslı arka yüz mimarilerini piksel hassasiyetinde ön yüz deneyimleriyle birleştirmede uzmanlaşmış Tam Yığın Geliştirici. Yapay zeka ve modern web teknolojileriyle geleceği inşa ediyor.

Gerçek Bir AI Agent Projesinde MCP Kullanırken Öğrendiklerim | Ömer Özbay Günlük | Ömer Özbay