İçeriğe atla

2025-12-10

Custom MCP Server Geliştirme: Production-Ready Kılavuz

TypeScript ile organizasyonunuzun internal sistemleri için custom Model Context Protocol serverları nasıl geliştirip, güvenli hale getirip, deploy edeceğinizi öğren. Authentication, monitoring ve Kubernetes deployment örnekleriyle.

Abstract

Model Context Protocol (MCP), büyük AI providerlar arasında hızla standart haline geldi. GitHub ya da Slack gibi yaygın servisler için hazır serverlar işe yarasa da, organizasyonların internal API’leri entegre etmek, güvenlik politikalarını uygulamak ve domain-specific logic’i encode etmek için custom serverlar gerekiyor. Bu rehber, TypeScript ile production-ready custom MCP server geliştirmeyi anlatıyor: başlangıç setup’tan Kubernetes deployment’a kadar. Authentication, circuit breaker’lar, audit logging ve monitoring için çalışan kod örnekleriyle.

Custom Integration Challenge

AI-assisted workflow’ları benimseyen organizasyonlar, hazır MCP serverlarla hızla sınırlara çarpıyor. Ekibin proprietary internal API’leri, custom database’leri ve legacy sistemleri var; bunlar için public MCP serverları yok. Generic serverlar senin validation kurallarını, güvenlik gereksinimlerini ya da compliance ihtiyaçlarını encode edemiyor.

Bir internal deployment sistemini düşün. Workflow şunları gerektiriyor: custom configuration service’ten prerequisite’ları kontrol etmek, LDAP üzerinden user permission’larını validate etmek, circuit breaker’lı internal API üzerinden deployment’ları trigger etmek, compliance için tüm aksiyonları log’lamak ve multi-region deployment koordinasyonunu handle etmek. Hiçbir hazır MCP server bu workflow’u anlamıyor.

Custom MCP serverlar geliştirmek şunları sağlıyor: tailored integration, protocol seviyesinde güvenlik enforcement, context window verimliliğini maksimize eden optimize edilmiş response’lar ve audit sistemleriyle compliance entegrasyonu.

Proje Yapısı ve Setup

İyi organize edilmiş bir proje yapısıyla başla: concern’ları ayır:

deployment-mcp-server/
├── src/
│  ├── index.ts  # Server initialization
│  ├── tools/  # Tool implementations
│  │  ├── check-prerequisites.ts
│  │  └── trigger-deployment.ts
│  ├── resources/  # Resource implementations
│  │  └── deployment-config.ts
│  ├── lib/
│  │  ├── api-client.ts  # Internal API wrapper
│  │  ├── auth.ts  # Authentication logic
│  │  └── validation.ts  # Shared validation
│  └── types/
│  └── deployment.ts  # TypeScript types
├── tests/
├── tsconfig.json
└── package.json

Gerekli dependency’lerle projeyi initialize et:

mkdir custom-mcp-server && cd custom-mcp-server
npm init -y

# Core dependencies
npm install @modelcontextprotocol/sdk zod axios

# Development dependencies
npm install -D typescript @types/node vitest tsx

Modern Node.js için TypeScript config’i:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}

Core Server Implementation

Server initialization, transport setup’ı, tool registration’ı ve graceful shutdown’ı handle ediyor:

// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { checkPrerequisitesTool } from "./tools/check-prerequisites.js";
import { triggerDeploymentTool } from "./tools/trigger-deployment.js";
import { deploymentConfigResource } from "./resources/deployment-config.js";

const server = new McpServer({
  name: "deployment-server",
  version: "1.0.0",
});

// Register tools
checkPrerequisitesTool(server);
triggerDeploymentTool(server);

// Register resources
deploymentConfigResource(server);

// Error handling
process.on('SIGINT', async () => {
  console.error('Shutting down gracefully...');
  await server.close();
  process.exit(0);
});

process.on('unhandledRejection', (error) => {
  console.error('Unhandled rejection:', error);
  process.exit(1);
});

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('Deployment MCP server running on stdio');
}

main().catch((error) => {
  console.error('Fatal error:', error);
  process.exit(1);
});

Buradaki key pattern’ler: modular tool registration codebase’i sürdürülebilir tutuyor, proper error handling silent failure’ları önlüyor ve logging için console.error() kullanmak kritik: stdout protocol message’ları için ayrılmış, başka output JSON-RPC stream’ini bozuyor.

Domain Logic ile Tool Implementation

Tool’lar organizasyonunun business rule’larını encode ediyor. İşte deployment prerequisite’larını validate eden kapsamlı bir örnek:

// src/tools/check-prerequisites.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { ConfigService } from "../lib/config-service.js";

// Domain-specific validation schema
const CheckPrerequisitesSchema = z.object({
  service: z.string().describe("Service name to deploy"),
  environment: z.enum(["development", "staging", "production"])
    .describe("Target environment"),
  version: z.string()
    .regex(/^\d+\.\d+\.\d+$/, "Must be semantic version (e.g., 1.2.3)")
    .describe("Version to deploy"),
});

export function checkPrerequisitesTool(server: McpServer) {
  server.tool(
    "check_deployment_prerequisites",
    CheckPrerequisitesSchema,
    async ({ service, environment, version }) => {
      const results: string[] = [];
      const errors: string[] = [];

      try {
        // 1. Service configuration'ı kontrol et
        const config = await ConfigService.getServiceConfig(service, environment);
        if (!config) {
          errors.push(`No configuration found for ${service} in ${environment}`);
        } else {
          results.push(`Configuration found for ${service}`);
        }

        // 2. Version compatibility'yi validate et
        const compatibilityCheck = await ConfigService.checkVersionCompatibility(
          service,
          version,
          environment
        );

        if (!compatibilityCheck.compatible) {
          errors.push(
            `Version ${version} incompatible: ${compatibilityCheck.reason}`
          );
        } else {
          results.push(`Version ${version} is compatible`);
        }

        // 3. Dependent service'lerin health'ini kontrol et
        const dependencies = await ConfigService.getDependencies(service);
        for (const dep of dependencies) {
          const health = await ConfigService.checkServiceHealth(dep, environment);
          if (!health.healthy) {
            errors.push(`Dependency ${dep} is unhealthy: ${health.status}`);
          } else {
            results.push(`Dependency ${dep} is healthy`);
          }
        }

        // 4. Deployment window'u validate et (production only)
        if (environment === "production") {
          const inWindow = await ConfigService.isInDeploymentWindow();
          if (!inWindow) {
            errors.push(
              "Outside deployment window (Mon-Thu 10AM-4PM EST)"
            );
          } else {
            results.push("Within deployment window");
          }
        }

        // Response'u formatla
        const hasErrors = errors.length > 0;
        const summary = hasErrors
          ? `Prerequisites check FAILED (${errors.length} issues)`
          : `All prerequisites passed (${results.length} checks)`;

        return {
          content: [
            {
              type: "text",
              text: [
                summary,
                "",
                "Checks Passed:",
                ...results.map(r => `  ${r}`),
                ...(hasErrors ? ["", "Issues Found:", ...errors.map(e => `  ${e}`)] : []),
                "",
                hasErrors ? " Deployment should NOT proceed" : "Safe to proceed with deployment",
              ].join("\n"),
            },
          ],
          isError: hasErrors,
        };
      } catch (error) {
        console.error("Prerequisites check failed:", error);
        return {
          content: [
            {
              type: "text",
              text: `Error checking prerequisites: ${error.message}`,
            },
          ],
          isError: true,
        };
      }
    }
  );
}

Bu birkaç domain logic pattern’i gösteriyor: Zod regex ile semantic version validation, clear pass/fail feedback’li multi-step prerequisite check’ler, production için deployment window’ları gibi environment-specific rule’lar, dependency health validation ve AI consumption için optimize edilmiş human-readable output.

Resilience ile API Integration

Internal API’lerle robust entegrasyon, retry’lar, circuit breaking ve proper error handling gerektiriyor:

// src/lib/api-client.ts
import axios, { AxiosInstance, AxiosError } from "axios";

interface CircuitBreakerState {
  failures: number;
  lastFailureTime: number;
  state: "closed" | "open" | "half-open";
}

export class InternalAPIClient {
  private client: AxiosInstance;
  private circuitBreaker: Map<string, CircuitBreakerState> = new Map();
  private readonly FAILURE_THRESHOLD = 5;
  private readonly TIMEOUT_MS = 30000;
  private readonly RESET_TIMEOUT_MS = 60000;

  constructor() {
    this.client = axios.create({
      baseURL: process.env.INTERNAL_API_URL,
      timeout: this.TIMEOUT_MS,
      headers: {
        "User-Agent": "deployment-mcp-server/1.0.0",
      },
    });

    // Authentication interceptor ekle
    this.client.interceptors.request.use(async (config) => {
      const token = await this.getAuthToken();
      config.headers.Authorization = `Bearer ${token}`;
      return config;
    });

    // Retry interceptor ekle
    this.client.interceptors.response.use(
      (response) => response,
      async (error: AxiosError) => {
        const config = error.config;

        // Circuit açıksa retry yapma
        if (this.isCircuitOpen(config.url)) {
          throw new Error(`Circuit breaker open for ${config.url}`);
        }

        // 5xx error'lar ya da network issue'larda retry yap
        if (
          error.response?.status >= 500 ||
          error.code === "ECONNABORTED" ||
          error.code === "ENOTFOUND"
        ) {
          const retryCount = (config as any).__retryCount || 0;

          if (retryCount < 3) {
            (config as any).__retryCount = retryCount + 1;

            // Exponential backoff
            const delay = Math.min(1000 * Math.pow(2, retryCount), 10000);
            await new Promise((resolve) => setTimeout(resolve, delay));

            console.error(
              `Retrying ${config.url} (attempt ${retryCount + 1}/3)`
            );

            return this.client.request(config);
          }

          // Max retry aşıldı - failure kaydet
          this.recordFailure(config.url);
        }

        throw error;
      }
    );
  }

  private isCircuitOpen(endpoint: string): boolean {
    const state = this.circuitBreaker.get(endpoint);
    if (!state || state.state === "closed") return false;

    // Timeout geçti mi kontrol et
    const elapsed = Date.now() - state.lastFailureTime;
    if (elapsed > this.RESET_TIMEOUT_MS) {
      // Half-open dene
      state.state = "half-open";
      state.failures = 0;
      return false;
    }

    return state.state === "open";
  }

  private recordFailure(endpoint: string): void {
    const state = this.circuitBreaker.get(endpoint) || {
      failures: 0,
      lastFailureTime: 0,
      state: "closed",
    };

    state.failures++;
    state.lastFailureTime = Date.now();

    if (state.failures >= this.FAILURE_THRESHOLD) {
      state.state = "open";
      console.error(
        `Circuit breaker OPEN for ${endpoint} (${state.failures} failures)`
      );
    }

    this.circuitBreaker.set(endpoint, state);
  }

  private async getAuthToken(): Promise<string> {
    // Token'ı cache'le
    const cached = this.tokenCache.get("access_token");
    if (cached && cached.expiresAt > Date.now()) {
      return cached.token;
    }

    // Yeni token al (OAuth2 client credentials)
    const response = await axios.post(
      `${process.env.AUTH_URL}/oauth/token`,
      {
        grant_type: "client_credentials",
        client_id: process.env.API_CLIENT_ID,
        client_secret: process.env.API_CLIENT_SECRET,
        scope: "deployments:read deployments:write",
      }
    );

    const token = response.data.access_token;
    const expiresIn = response.data.expires_in;

    this.tokenCache.set("access_token", {
      token,
      expiresAt: Date.now() + expiresIn * 1000,
    });

    return token;
  }

  async triggerDeployment(params: {
    service: string;
    version: string;
    environment: string;
  }): Promise<{ deploymentId: string; status: string }> {
    try {
      const response = await this.client.post("/deployments", params);
      return response.data;
    } catch (error) {
      if (axios.isAxiosError(error)) {
        // API error'larını user-friendly message'lara transform et
        if (error.response?.status === 403) {
          throw new Error(
            "Insufficient permissions for deployment. Contact DevOps team."
          );
        }
        if (error.response?.status === 409) {
          throw new Error(
            `Deployment conflict: ${error.response.data.message}`
          );
        }
        throw new Error(
          `Deployment API error: ${error.response?.data?.message || error.message}`
        );
      }
      throw error;
    }
  }

  private tokenCache = new Map<string, { token: string; expiresAt: number }>();
}

Bu implementation şunları içeriyor: cascading failure’ları önleyen circuit breaker’lar, jitter’lı exponential backoff, auth overhead’i azaltan token caching, user-friendly error transformation ve timeout protection. Circuit breaker’ların önemini, bu pattern’ler implement edilmeden önce backend outage’ın MCP server’ı 20 dakika boyunca down ettiği bir durumda öğrendim.

Security: Authentication ve Audit Logging

Güvenlik sonradan eklenemez. Authentication, authorization ve audit logging’i baştan tasarla:

// src/lib/auth.ts
import { z } from "zod";

interface UserContext {
  userId: string;
  email: string;
  groups: string[];
  permissions: Set<string>;
}

export class AuthService {
  private static ldapClient: LDAPClient;
  private static userCache = new Map<string, { user: UserContext; expiresAt: number }>();

  static async authenticateUser(token: string): Promise<UserContext> {
    // Cache kontrol et
    const cached = this.userCache.get(token);
    if (cached && cached.expiresAt > Date.now()) {
      return cached.user;
    }

    // Token'ı auth provider ile validate et
    const decoded = await this.verifyJWT(token);

    // LDAP group'ları ve permission'ları load et
    const ldapUser = await this.ldapClient.search({
      filter: `(mail=${decoded.email})`,
      attributes: ["cn", "memberOf"],
    });

    const groups = ldapUser.memberOf.map(dn => this.extractGroupName(dn));
    const permissions = await this.loadPermissionsForGroups(groups);

    const user: UserContext = {
      userId: decoded.sub,
      email: decoded.email,
      groups,
      permissions: new Set(permissions),
    };

    // 5 dakika cache'le
    this.userCache.set(token, {
      user,
      expiresAt: Date.now() + 5 * 60 * 1000,
    });

    return user;
  }

  static authorizeEnvironment(
    user: UserContext,
    environment: "development" | "staging" | "production"
  ): void {
    const permissionMap = {
      development: "deploy:dev",
      staging: "deploy:staging",
      production: "deploy:production",
    };

    if (!user.permissions.has(permissionMap[environment])) {
      throw new Error(
        `Access denied: missing permission '${permissionMap[environment]}'`
      );
    }

    // Production için ekstra group membership gerekli
    if (environment === "production") {
      if (!user.groups.includes("production-deployers")) {
        throw new Error(
          "Production deployments require 'production-deployers' group membership"
        );
      }
    }
  }
}

export class AuditLogger {
  private static auditQueue: AuditEvent[] = [];
  private static flushInterval: NodeJS.Timeout;

  static init() {
    // Her 10 saniyede audit log'ları flush et
    this.flushInterval = setInterval(() => this.flush(), 10000);
  }

  static log(event: {
    action: string;
    user: UserContext;
    resource: string;
    result: "success" | "failure" | "denied";
    metadata?: Record<string, any>;
  }): void {
    const auditEvent: AuditEvent = {
      timestamp: new Date().toISOString(),
      userId: event.user.userId,
      userEmail: event.user.email,
      action: event.action,
      resource: event.resource,
      result: event.result,
      metadata: event.metadata,
      ipAddress: process.env.CLIENT_IP || "unknown",
      serverVersion: "1.0.0",
    };

    this.auditQueue.push(auditEvent);

    console.error(
      `AUDIT: ${auditEvent.action} on ${auditEvent.resource} by ${auditEvent.userEmail}: ${auditEvent.result}`
    );

    // Critical event'ler için hemen flush et
    if (event.result === "denied" || event.action.includes("production")) {
      this.flush();
    }
  }

  private static async flush(): Promise<void> {
    if (this.auditQueue.length === 0) return;

    const batch = [...this.auditQueue];
    this.auditQueue = [];

    try {
      await axios.post(process.env.AUDIT_API_URL, {
        events: batch,
        source: "deployment-mcp-server",
      });
    } catch (error) {
      console.error("Failed to flush audit logs:", error);
      // Başarısız event'leri yeniden kuyruğa al
      this.auditQueue.unshift(...batch);
    }
  }

  static shutdown(): void {
    clearInterval(this.flushInterval);
    this.flush();
  }
}

interface AuditEvent {
  timestamp: string;
  userId: string;
  userEmail: string;
  action: string;
  resource: string;
  result: "success" | "failure" | "denied";
  metadata?: Record<string, any>;
  ipAddress: string;
  serverVersion: string;
}

Production için HTTP Transport

stdio transport local development için işe yarasa da, production deployment’lar HTTP transport gerektiriyor: multiple concurrent client’lar, bağımsız server lifecycle, load balancing ve standart monitoring için:

// src/http-server.ts
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { randomUUID } from "crypto";
import rateLimit from "express-rate-limit";
import helmet from "helmet";

const app = express();
const mcpServer = new McpServer({
  name: "deployment-server",
  version: "1.0.0",
});

// Security middleware
app.use(helmet());
app.use(express.json({ limit: "1mb" }));

// Rate limiting
const limiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100,
  message: "Too many requests from this IP, please try again later",
});

app.use("/mcp", limiter);

// Authentication middleware
app.use("/mcp", async (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Missing or invalid authorization header" });
  }

  const token = authHeader.substring(7);

  try {
    const user = await AuthService.authenticateUser(token);
    req.user = user;
    next();
  } catch (error) {
    console.error("Authentication failed:", error);
    return res.status(401).json({ error: "Invalid token" });
  }
});

// HTTP transport oluştur
const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: () => randomUUID(),
  enableJsonResponse: true,
});

// MCP message endpoint
app.post("/mcp/message", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string;

  AuditLogger.log({
    action: "mcp_message",
    user: req.user,
    resource: "mcp-server",
    result: "success",
    metadata: { sessionId, method: req.body.method },
  });

  await transport.handleMessage(req, res);
});

// Health check endpoint
app.get("/health", (req, res) => {
  res.json({
    status: "healthy",
    version: "1.0.0",
    uptime: process.uptime(),
  });
});

// Readiness check (Kubernetes için)
app.get("/ready", async (req, res) => {
  try {
    await Promise.race([
      ConfigService.healthCheck(),
      new Promise((_, reject) =>
        setTimeout(() => reject(new Error("Timeout")), 5000)
      ),
    ]);

    res.json({ ready: true });
  } catch (error) {
    res.status(503).json({ ready: false, error: error.message });
  }
});

const PORT = process.env.PORT || 3000;

async function startServer() {
  await mcpServer.connect(transport);

  app.listen(PORT, () => {
    console.error(`MCP HTTP server listening on port ${PORT}`);
    console.error(`Health check: http://localhost:${PORT}/health`);
  });
}

startServer().catch((error) => {
  console.error("Failed to start server:", error);
  process.exit(1);
});

Kubernetes Deployment

Kubernetes ile container deployment, high availability ve scalability sağlıyor:

# Dockerfile
FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

FROM node:20-alpine

WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

RUN addgroup -g 1001 -S mcp && \
    adduser -S -u 1001 -G mcp mcp

USER mcp

EXPOSE 3000

CMD ["node", "dist/http-server.js"]

Production deployment için Kubernetes manifest:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-mcp-server
  namespace: ai-tools
spec:
  replicas: 3
  selector:
    matchLabels:
      app: deployment-mcp-server
  template:
    metadata:
      labels:
        app: deployment-mcp-server
    spec:
      containers:
      - name: server
        image: your-registry/deployment-mcp-server:1.0.0
        ports:
        - containerPort: 3000
          name: http
        env:
        - name: NODE_ENV
          value: production
        - name: INTERNAL_API_URL
          valueFrom:
            configMapKeyRef:
              name: mcp-config
              key: api_url
        - name: API_CLIENT_SECRET
          valueFrom:
            secretKeyRef:
              name: mcp-secrets
              key: api_client_secret
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: deployment-mcp-server
  namespace: ai-tools
spec:
  selector:
    app: deployment-mcp-server
  ports:
  - port: 80
    targetPort: 3000
    name: http
  type: ClusterIP

Test Stratejisi

External dependency’leri mock’layan unit test’lerle MCP tool’larını etkili şekilde test et:

// tests/tools/check-prerequisites.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { checkPrerequisitesTool } from "../../src/tools/check-prerequisites.js";
import { ConfigService } from "../../src/lib/config-service.js";

vi.mock("../../src/lib/config-service.js");

describe("check_deployment_prerequisites tool", () => {
  let server: McpServer;

  beforeEach(() => {
    server = new McpServer({ name: "test", version: "1.0.0" });
    checkPrerequisitesTool(server);
    vi.clearAllMocks();
  });

  it("should pass all checks for valid deployment", async () => {
    vi.mocked(ConfigService.getServiceConfig).mockResolvedValue({
      name: "api-service",
      currentVersion: "1.0.0",
    });

    vi.mocked(ConfigService.checkVersionCompatibility).mockResolvedValue({
      compatible: true,
      reason: "",
    });

    vi.mocked(ConfigService.getDependencies).mockResolvedValue([
      "database-service",
    ]);

    vi.mocked(ConfigService.checkServiceHealth).mockResolvedValue({
      healthy: true,
      status: "operational",
    });

    const result = await server._callTool("check_deployment_prerequisites", {
      service: "api-service",
      environment: "staging",
      version: "1.2.3",
    });

    expect(result.isError).toBe(false);
    expect(result.content[0].text).toContain("All prerequisites passed");
  });

  it("should fail when dependency is unhealthy", async () => {
    vi.mocked(ConfigService.getServiceConfig).mockResolvedValue({
      name: "api-service",
    });

    vi.mocked(ConfigService.checkVersionCompatibility).mockResolvedValue({
      compatible: true,
    });

    vi.mocked(ConfigService.getDependencies).mockResolvedValue([
      "database-service",
    ]);

    vi.mocked(ConfigService.checkServiceHealth).mockResolvedValue({
      healthy: false,
      status: "degraded",
    });

    const result = await server._callTool("check_deployment_prerequisites", {
      service: "api-service",
      environment: "staging",
      version: "1.2.3",
    });

    expect(result.isError).toBe(true);
    expect(result.content[0].text).toContain("database-service is unhealthy");
  });
});

Yaygın Hatalar ve Öğrenilenler

Stdout/Stderr Karışıklığı

MCP, stdout’u yalnızca JSON-RPC message’ları için kullanıyor. Başka output, protocol stream’ini bozuyor:

// YANLIŞ - protocol'ü bozar
console.log("Processing deployment...");

// DOĞRU - tüm log'lar stderr'e
console.error("Processing deployment...");

Client’ın “Protocol error” ya da “Invalid JSON” göstermesi durumunda, stdout pollution’ı kontrol et.

Missing Input Validation

AI modelleri beklenmedik ya da malicious input’lar üretebiliyor. Strict validation için Zod refinement’ları kullan:

// YANLIŞ - çok permissive
server.tool(
  "delete_service",
  z.object({ service: z.string() }),
  async ({ service }) => {
    await api.delete(`/services/${service}`); // Path traversal risk
  }
);

// DOĞRU - strict validation
server.tool(
  "delete_service",
  z.object({
    service: z.string()
      .regex(/^[a-z0-9-]+$/)
      .min(3)
      .max(50),
    confirmation: z.literal("DELETE"),
  }),
  async ({ service, confirmation }) => {
    const exists = await api.serviceExists(service);
    if (!exists) {
      throw new Error(`Service ${service} not found`);
    }
    await api.delete(`/services/${service}`);
  }
);

Context Window Bloat

Her token, context window’u tüketiyor. AI, conversation başına tool’ları onlarca kez invoke edebiliyor. Yalnızca ilgili field’ları return et:

// YANLIŞ - 100+ field return ediyor
const user = await api.getUser(id);
return { content: [{ type: "text", text: JSON.stringify(user) }] };

// DOĞRU - yalnızca essential field'ları return et
const user = await api.getUser(id);
return {
  content: [{
    type: "text",
    text: JSON.stringify({
      id: user.id,
      name: user.name,
      email: user.email,
      status: user.status,
    }),
  }],
};

Bu yaklaşım, test’lerde token kullanımını yaklaşık %70 azalttı.

Synchronous Long Operation’lar

Tool’lar 5-10 saniye içinde return etmeli. Daha uzun operation’lar için task-based pattern kullan:

// YANLIŞ - dakikalarca block ediyor
server.tool("deploy_service", schema, async (params) => {
  await runDeployment(params); // 3-5 dakika sürüyor
  return { content: [{ type: "text", text: "Deployment complete" }] };
});

// DOĞRU - async task pattern
server.tool("start_deployment", schema, async (params) => {
  const taskId = await deploymentQueue.enqueue(params);

  return {
    content: [{
      type: "text",
      text: `Deployment started with ID: ${taskId}\nUse check_deployment_status to monitor progress`,
    }],
  };
});

server.tool("check_deployment_status", z.object({ taskId: z.string() }), async ({ taskId }) => {
  const status = await deploymentQueue.getStatus(taskId);
  return {
    content: [{
      type: "text",
      text: `Deployment ${taskId}: ${status.state}\nProgress: ${status.progress}%`,
    }],
  };
});

Maliyet Analizi

Development: Production-ready server için 1-2 hafta (server development 3-5 gün, security 1-2 gün, testing 1 gün, deployment 1-2 gün, dokümantasyon 1 gün).

Infrastructure (AWS örneği):

  • Kubernetes: ~130/ay(EKSallocation130/ay (EKS allocation 50, EC2 instance’lar 45,loadbalancer45, load balancer 20, secret/log’lar $25)
  • Serverless (Fargate): ~$50/ay (düşük maliyet, yüksek cold start latency)

Custom ne zaman build edilmeli: Internal/proprietary sistemler, strict güvenlik/compliance gereksinimleri, domain-specific validation gerekli, context optimization kritik, yüksek integration volume.

Pre-built ne zaman kullanılmalı: Standart integration’lar (GitHub, Slack), hızlı prototipleme, düşük güvenlik gereksinimleri, sınırlı development kaynakları.

Temel Çıkarımlar

stdio transport ve basic tool’larla basit başla, sonra güvenlik ve production feature’larını incremental ekle. Authentication, authorization ve audit logging’i baştan tasarla. Güvenliği sonradan eklemek zor.

Her response’u optimize et. Context window verimliliğini maksimize etmek için yalnızca ilgili field’ları return et. Strict input validation için Zod kullan ve backend response’larını validate et. AI-generated input’lara ya da backend API’lerine güvenme.

Backend’ler unhealthy olduğunda cascading failure’ları önlemek için circuit breaker’lar şart. Yeniden kullanılabilir ve test edilebilir küçük, focused tool’lar geliştir; AI’ın tool composition ile complex workflow’ları orkestre etmesine izin ver.

Production, observability gerektiriyor: metric’ler, logging ve alerting opsiyonel değil. Aynı code’u farklı config’lerle production-like environment’larda test et, production’dan önce issue’ları yakala.

MCP server geliştirme 1-2 hafta sürüyor, custom REST API’ler 2-3 hafta. Standardization, multi-provider support ve büyüyen ekosistem sayesinde hızla geri dönüş yapıyor.

İlgili yazılar

MCP İleri Düzey Kalıplar: Yetenekler, İş Akışları, Entegrasyon ve RBAC

Model Context Protocol implementasyonları için kurumsal düzeyde kalıplar: araç bileşimi, çoklu ajan orkestrasyonu, rol tabanlı erişim kontrolü ve production gözlemlenebilirlik.

mcpai-integrationrbac+4
TypeScript AI SDK Karşılaştırması: Agent Geliştirme için Vercel AI SDK vs OpenAI Agents SDK

AI agent geliştirmek için TypeScript SDK'larının pratik karşılaştırması - Vercel AI SDK, OpenAI Agents SDK ve AWS Bedrock entegrasyonu. Kod örnekleri, karar frameworkleri ve production patternleri içeriyor.

typescriptai-toolsserverless+4
AWS Secrets Manager & Parameter Store: Güvenlik Best Practices

AWS Secrets Manager ve Systems Manager Parameter Store'u karşılaştıran kapsamlı teknik rehber - hangi servisi ne zaman kullanmalı ve gerçek dünya implementation pattern'leri.

awssecrets-managerparameter-store+8
Type-Safe Lambda Middleware: Middy, Zod ve Builder Pattern ile Enterprise Uygulamalar

Middy builder pattern, Zod validation, feature flags ve secrets management kullanarak enterprise serverless uygulamaları için sürdürülebilir, type-safe Lambda middleware nasıl inşa edilir öğren.

aws-lambdamiddymiddleware+8
AWS CDK Link Shortener 3. Bölüm: Gelişmiş Özellikler ve Güvenlik

Custom domain'ler, toplu işlemler, URL expiration ve kapsamlı güvenlik önlemlerinin implementasyonu. Production link shortener servisleri için defense-in-depth güvenlik stratejileri.

aws-cdklambdasecurity+6