Anatomi Masalah: MCP Server Hang dan Deployment Stall

Integrasi Model Context Protocol (MCP) pada Claude Code memungkinkan AI mengakses tooling internal via protokol RPC berbasis JSON-RPC melalui stdio atau SSE (Server-Sent Events). Perbedaan mendasar antara CLI wrapper konvensional dan MCP server terletak pada siklus hidup proses (process lifecycle). CLI wrapper dieksekusi sekali dan langsung terminasi (ephemeral), sedangkan MCP server berjalan sebagai long-running daemon.

Ketika plugin MCP mengalami unhandled promise rejection, deadlock koneksi basis data, atau blocking synchronous I/O, proses server tidak mengembalikan respons JSON-RPC dan tetap menahan channel stdio. Kondisi ini menyebabkan pipeline CI/CD atau runtime Claude Code mengalami stall tak terbatas (hanging), menghabiskan runner resources dan memblokir alur rilis.

Langkah 1: Observasi via Structured Logs dan Telemetri Proses

Identifikasi masalah memerlukan inspeksi status proses dan parsing payload log JSON-RPC. Claude Code memetakan server MCP melalui konfigurasi lokal atau project (claude_desktop_config.json atau .claude.json).

Deteksi Deadlock Proses MCP

Jalankan perintah berikut untuk mengidentifikasi proses zombie atau hang yang menahan port atau child process:

# Deteksi proses child MCP yang terikat pada parent Claude Code
ps aux | grep -E "(claude|mcp-server)" | grep -v grep

# Inspeksi status file descriptor dan stdio buffer yang tersumbat
lsof -p <PID_MCP_SERVER> -a -d 0,1,2

Parsing Structured Logs MCP

Saat stdout bercampur dengan log non-protokol, framing JSON-RPC rusak. Amati aliran log runtime untuk memvalidasi ketiadaan respons payload:

# Filter log error stderr Claude Code
tail -n 100 -f ~/.claude/logs/mcp*.log | jq -R 'fromjson? | select(.level == "error" or .error != null)'

Tanda pasti dari server hang adalah request tools/call tercatat terkirim tanpa adanya pasangan return packet tools/call result hingga timeout tercapai.

Langkah 2: Verifikasi Status Kegagalan Pipeline

Sebelum melakukan intervensi, bedakan antara kegagalan autentikasi upstream, kegagalan transport stdio, dan internal runtime exception. Eksekusi direct healthcheck ping langsung ke interface stdio proses tanpa melewati Claude Code:

# Kirim payload JSON-RPC initialize secara manual untuk menguji responsivitas
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"healthcheck","version":"1.0"}}}' | node ./dist/index.js

# Exit code 1 atau silent hang mengonfirmasi kegagalan runtime MCP server

Langkah 3: Eksekusi Safe Rollback ke CLI Wrapper Statis

Pendekatan mitigasi tercepat (lightest-first) adalah menurunkan dependensi runtime dinamis MCP server kembali ke CLI wrapper statis berbasis command execution. Langkah ini mengeliminasi layer long-running daemon dan memulihkan reliabilitas pipeline.

1. Nonaktifkan Plugin MCP pada Konfigurasi

Edit konfigurasi tooling (misalnya claude.json atau settings tooling runner) untuk mencopot referensi MCP server yang bermasalah:

{
  "mcpServers": {
    // Hapus atau nonaktifkan server yang bermasalah:
    // "internal-tooling": { "command": "node", "args": ["/opt/mcp/index.js"] }
  }
}

2. Implementasikan Fallback CLI Wrapper Statis

Gantikan pemanggilan via script wrapper deterministik dengan mekanisme timeout native OS:

#!/usr/bin/env bash
# bin/tool-runner-fallback.sh
set -euo pipefail

TIMEOUT_SEC=15

# Eksekusi task statis dengan batasan timeout tegas
if ! timeout "${TIMEOUT_SEC}" npx --yes @internal/legacy-cli "$@"; then
  echo "{\"error\": \"Execution timed out or failed with exit code $?\"}" >&2
  exit 1
fi
Catatan Trade-off: Rollback ke CLI wrapper memutus fitur dynamic capability negotiation dan dynamic schema inference bawaan MCP. AI akan kehilangan kemampuan auto-discovery arguments, tetapi kontinuitas eksekusi pipeline tetap aman dari blocking issue.

Langkah 4: Mitigasi Pasca-Insiden (Postmortem & Hardening)

Untuk mencegah regresi berulang saat MCP server diintegrasikan kembali, terapkan dua kontrol berikut pada layer implementasi server:

1. Defensif Process Timeout Wrapper (Server-Side)

Bungkus execution handlers dengan abort controller dan structured exception boundary:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";

const EXECUTION_TIMEOUT_MS = 10000;

async function handleToolCallWithTimeout(request, handler) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), EXECUTION_TIMEOUT_MS);

  try {
    return await Promise.race([
      handler(request, { signal: controller.signal }),
      new Promise((_, reject) => {
        controller.signal.addEventListener("abort", () => 
          reject(new Error(`Tool execution timed out after ${EXECUTION_TIMEOUT_MS}ms`))
        );
      })
    ]);
  } finally {
    clearTimeout(timer);
  }
}

2. Standardisasi Strict Stdio Isolation

Pastikan kode aplikasi dan dependensi pihak ketiga tidak pernah menulis log diagnostik mentah ke process.stdout. Seluruh log telemetri wajib diarahkan ke process.stderr agar struktur paket JSON-RPC tidak korup:

// Anti-pattern: console.log merusak format payload transport stdio MCP
// console.log("Debug event triggered");

// Best practice: tulis telemetri ke stderr
process.stderr.write(JSON.stringify({ level: "debug", message: "Task processed" }) + "\n");

Ringkasan Tindakan Darurat

  1. Deteksi: Periksa hanging PID dan ketiadaan return packet JSON-RPC pada log.
  2. Isolasi: Matikan proses hung (kill -15 <PID>) dan bypass MCP server dari konfigurasi.
  3. Rollback: Alihkan eksekusi ke CLI wrapper statis ber-timeout untuk memulihkan rilis.
  4. Hardening: Pasang AbortController timeout di dalam server MCP dan pisahkan stdout/stderr secara ketat.