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.
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#
- Model Context Protocol - Resmi Dokümantasyon (yeni sekmede açılır) - Transport’ları, araçları, kaynakları ve güvenlik gereksinimlerini kapsayan yetkili protokol spesifikasyonu
- GitHub - modelcontextprotocol/servers (yeni sekmede açılır) - Yaygın entegrasyonlar için üretim pattern’lerini gösteren referans MCP sunucu uygulamaları
- GitHub - modelcontextprotocol/typescript-sdk (yeni sekmede açılır) - Node.js üzerinde MCP sunucu ve istemcilerini uygulamak için resmi TypeScript SDK’sı
- MCP Inspector - Model Context Protocol (yeni sekmede açılır) - AI istemcilerine bağlanmadan önce protokol uyumluluğunu doğrulamak için interaktif hata ayıklama aracı
- GitHub - modelcontextprotocol/python-sdk (yeni sekmede açılır) - Tam spesifikasyon kapsamıyla MCP sunucular için resmi Python SDK’sı
İlgili yazılar
Secrets Manager ve Parameter Store'u karşılaştıran teknik rehber: hangi servisi ne zaman seçmeli ve production implementation pattern'leri.
aws · secrets-management · security +7
Tek bir PII branded type'ı observability API imzalarınıza yerleştirin; TypeScript hassas alanları runtime redactor görmeden, çağrı yerinde reddetsin.
typescript · zod · observability +3
Webhook, imzalı URL ve servisler arası kimlik doğrulama için pratik bir HMAC-SHA-256 rehberi: üç dilde çalışan kod ve dijital imzaya geçmenin gerektiği sınır.
security · encryption · webhooks +4
AWS Cognito ve Verified Permissions ile SaaS yetkilendirmeyi Cedar politikaları, çok kiracılı desenler, JWT akışı ve maliyet analiziyle TypeScript'te kurun.
authorization · aws · authentication +4
Model Context Protocol için kurumsal kalıplar: araç bileşimi, çoklu ajan orkestrasyonu, rol tabanlı erişim kontrolü ve production gözlemlenebilirlik.
mcp · ai-adoption-strategy · authorization +4