İçeriğe atla

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

TypeScript ile internal sistemleriniz için custom MCP server geliştirin: authentication, monitoring ve Kubernetes deployment örnekleriyle uçtan uca rehber.

Ayhan Sipahi Ayhan Sipahi

Model Context Protocol (MCP), büyük AI sağlayıcıların ortak entegrasyon yüzeyi haline geldi. GitHub ya da Slack gibi açık servisler için hazır server’lar iş görüyor. Internal API’ler, proprietary database’ler ve legacy sistemler içinse böyle bir server yok; generic bir server da senin validation kurallarını, güvenlik gereksinimlerini ya da compliance ihtiyaçlarını encode edemiyor. Bu sistemlerde varsayılan, kendi server’ını yazmak. İş de göründüğünden küçük: bir TypeScript projesi, iyi tanımlanmış birkaç tool ve zaten her internal servise uyguladığın production disiplini.

Aşağıdaki örnek, bir internal deployment sistemi. Workflow, custom configuration service’ten prerequisite’ları okuyor, LDAP üzerinden user permission’larını doğruluyor, circuit breaker arkasındaki internal API üzerinden deployment tetikliyor, compliance için her aksiyonu logluyor ve bölgeler arası koordinasyonu yürütüyor. Hiçbir hazır server bu workflow’u anlamıyor; custom server’ın kazandırdığı da tam olarak bunu encode edebilmek: tailored entegrasyon, protocol seviyesinde güvenlik enforcement, context window’u koruyacak şekilde kırpılmış response’lar ve compliance ekibinin sorgulayabileceği audit kayıtları.

Proje Yapısı ve Setup#

Concern’ları ayıran, iyi organize edilmiş bir proje yapısıyla başla:

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"]
}

Server Çekirdeği#

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);
});

Burada üç detay önemli. Modular tool registration, tool sayısı arttıkça codebase’i sürdürülebilir tutuyor; unhandledRejection handler’ı silent failure’ları önlüyor. Logging için console.error() kullanmak ise zorunlu, çünkü stdout protocol message’ları için ayrılmış ve başka her output JSON-RPC stream’ini bozuyor.

Domain Logic’i Tool’lara Gömmek#

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,
        };
      }
    }
  );
}

Kopyalanmaya değer dört pattern var. Zod regex’i semantic version’ı şema sınırında zorluyor, böylece bozuk bir version API’ye hiç ulaşmıyor. Prerequisite check’leri ilk hatada durmak yerine bütün hataları topluyor; model tek çağrıda tam resmi görüyor. Production deployment window’u gibi environment’a özgü kurallar, prompt yerine tool’un içinde duruyor. Çıktı da bir insan okuyacakmış gibi yazıldığı için model onu başka kelimelere dökmeden aktarabiliyor.

Dayanıklı API Entegrasyonu#

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 }>();
}

Client şunları bir araya getiriyor: cascading failure’ları durduran bir circuit breaker, 10 saniyeyle sınırlanmış exponential backoff, OAuth turlarını sıcak yoldan çıkaran token caching, 403’ü modelin işine yarayacak bir cümleye çeviren error transformation ve hepsinin altında duran request timeout’u. Circuit, bir endpoint’te üst üste beş failure’dan sonra açılıyor ve sonraki çağrının half-open denemesi olarak geçmesine izin verilene kadar bir dakika açık kalıyor. Bu breaker olmasa, sağlıksız bir backend her tool çağrısını 30 saniyelik bir beklemeye çeviriyor ve model durmasını söyleyen bir şey olmadığı için denemeye devam ediyor.

Güvenlik: 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#

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.

Eksik 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 Şişmesi#

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,
    }),
  }],
};

Kazanç birikimli. Aynı tool’u tek bir konuşmada on kez çağıran bir model, field listesinin bedelini on kez ödüyor; bu yüzden response’u tool sınırında kırpmak, prompt içinde kırpmaktan daha ucuz.

Uzun Süren Senkron İşlemler#

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}%`,
    }],
  };
});

Custom mu, Hazır mı#

Arkadaki sistem internal ya da proprietary’se, güvenlik ve compliance kuralları protocol sınırında uygulanmak zorundaysa, validation domain’e özgüyse veya response’un şekli context window verimliliğini bir tasarım hedefi yapacak kadar önemliyse custom server yaz. Yüksek entegrasyon hacmi de aynı yöne itiyor: elle yazdığın her tool, model onu her çağırdığında kendini yeniden amorti ediyor. Altyapı kararı nadiren belirliyor; küçük bir Node process’inin birkaç replica’sı, zaten çalıştırdığın cluster’da herhangi bir internal servis kadar tutuyor.

Hedef, resmi implementasyonu olan açık bir servisse, prototip aşamasındaysan ve entegrasyonun bir hafta sonra ayakta kalacağı belli değilse ya da server’ı yayına aldıktan sonra sahiplenecek kimse yoksa hazır server’ı tercih et. İki seçenekten kötüsü, sahipsiz kalan custom server: internal bir sistemin credential’larını taşıyor ve kimse başında değil.

Hangisini seçersen seç, inşa sırası aynı. stdio transport ve iki üç tool ile başla, bunları gerçek bir backend’e karşı çalıştır, sonra kullanım arttıkça authentication, audit logging ve HTTP transport ekle. Ertelenmemesi gereken tek şey authorization: kullanıcısı olan bir server’a sonradan eklemek, her tool’u ikinci kez denetlemek demek.

Kaynaklar#

İlgili yazılar