MCP의 일곱 대죄: 운영적 죄
I’m happy to translate the article for you, but I need the full text of the post (the part you’d like translated). Could you please paste the content you want translated here? I’ll keep the source line and all formatting exactly as you request.
운영상의 죄악: Sloth와 Wrath
이러한 죄악은 라이브 MCP 시스템이 스트레스 상황에서 어떻게 동작하는지를 결정하기 때문에 이 범주에 포함됩니다: 시스템이 정확하게 실패하는지, 정상적으로 복구되는지, 그리고 운영자가 장애 발생 시 화면에 표시되는 내용을 신뢰할 수 있는지 여부입니다.
두 죄악 모두 시스템에 부하가 걸릴 때 나타납니다.
- Sloth는 모호한 오류, 약한 검증, 혹은 부실한 전송 처리 뒤에 문제를 숨깁니다.
- Wrath는 복구 가능한 문제를 눈먼 재시도, 재연결 폭풍, 불확실성에 대한 강제 반응을 통해 증폭시킵니다.
접근 경계가 더 엄격해질수록, 다음 질문은 “문제가 발생했을 때 시스템이 어떻게 동작하는가?”가 됩니다. 바로 이때 두 죄악이 작동합니다.
운영 레이어는 MCP에서 특히 눈에 띕니다. 왜냐하면 전송 및 프로토콜 동작이 제품의 일부이기 때문입니다. Std‑io 위생, 재연결 동작, 재개 가능성, 알림, 세션 처리는 모델‑대면 인터페이스가 라이브가 되면 부수적인 세부 사항이 아닙니다.
Sloth
Sloth는 정확한 검증, 구체적인 오류, 기본적인 운영 위생을 회피하는 것입니다. 이 죄악은 코드 리뷰에서 크게 눈에 띄지 않을 때가 많으며, 보통 무해해 보입니다:
- catch 블록이 상세 정보를 숨깁니다.
- 검증 규칙이 연기됩니다.
- 디버그 출력이 잘못된 스트림으로 갑니다.
아무도 위험한 선택을 하고 있다고 생각하지 않습니다—시간을 절약하고 있다고 생각하죠. 하지만 MCP는 부실한 경계에 대해 관대하지 않습니다.
어떻게 발견할까
- 무언가 실패했을 때 로그가 모호하거나 반복적이며 쓸모가 없습니다.
- 운영자는 실제 실패 원인 대신 “MCP error”와 같은 일반 오류만 보게 됩니다.
- Std‑io 통합이 신비롭게 깨지는데, 이는 프로토콜이 아닌 출력이
stdout으로 새어나갔기 때문입니다. - 팀이 실패를 고치는 것보다 재현하는 데 더 많은 시간을 소비합니다.
예시
이 문제는 지원 및 운영 대기열에서 자주 나타납니다. 담당자가 채팅 중인 고객을 기다리게 하면서 어시스턴트에게 고객 cus_1234를 가져오라고 요청하거나, 엔지니어가 트라이아지 중에 ID로 최신 인시던트를 요청할 때가 그렇습니다. 그 순간 잘못된 입력, 찾을 수 없음, 의존성 장애는 각각 다른 상황이며 다음 단계도 다릅니다. 도구가 이들을 하나의 모호한 실패로 합쳐버리면 사용자는 올바르게 대응할 컨텍스트를 잃게 됩니다.
Before
server.tool("get_customer", async ({ id }) => {
try {
return await db.customers.findById(id);
} catch {
throw new Error("MCP error");
}
});After
class ToolError extends Error {
constructor(
public code: "invalid_input" | "not_found" | "dependency_unavailable",
message: string,
public retryable: boolean
) {
super(message);
}
}
server.tool("get_customer", async ({ id }) => {
if (typeof id !== "string" || id.trim() === "") {
throw new ToolError(
"invalid_input",
"id must be a non‑empty string",
false
);
}
try {
const customer = await db.customers.findById(id);
if (!customer) {
throw new ToolError("not_found", `customer ${id} not found`, false);
}
return customer;
} catch (error) {
console.error("get_customer failed", { id, error });
if (error instanceof ToolError) {
throw error;
}
throw new ToolError(
"dependency_unavailable",
"customer lookup is temporarily unavailable",
true
);
}
});전송 위생을 바로잡기
// Wrong for stdio servers
console.log("server started");
// Correct for stdio servers
console.error("server started");프로토콜 경계에서 실패를 솔직히 드러내기
// `code` and `retryable` are part of this server's error contract,
// not fields that MCP invents automatically for you.
function toMcpErrorResult(error: ToolError) {
return {
isError: true,
code: error.code,
retryable: error.retryable,
content: [{ type: "text", text: error.message }],
};
}해결 방법
수정은 경계에서 시작됩니다. 도구가 시작되는 지점에서 입력을 검증하고, 요청이 이미 복잡해진 후 호출 스택 깊숙이 들어가서 검증을 수행하지 마세요. 문제가 발생했을 때…
Source: …
fail, preserve the real failure mode whenever you can. Operators need useful errors, not vague theatrical ones, and callers need to know the difference between a not‑found result and a broken dependency.
실제 실패 모드를 가능한 한 유지하십시오. 운영자는 모호하고 연극적인 오류가 아니라 유용한 오류를 필요로 하며, 호출자는 not‑found 결과와 깨진 종속성 사이의 차이를 알아야 합니다.
In practice, this usually means:
실제로는 보통 다음을 의미합니다:
Stable error codes.
Clear human‑readable messages.
A separate place for internal diagnostic detail.
안정적인 오류 코드.
명확한 사람이 읽을 수 있는 메시지.
내부 진단 세부 정보를 위한 별도 위치.
Treat operational hygiene as part of the contract. Keep protocol traffic separate from diagnostics, especially on stdio where stdout is data and stderr is logs. On remote HTTP transports, the equivalent discipline is session lifecycle, reconnect behavior, and resumability: if those are inconsistent, the system becomes hard to reason about even when the handlers themselves are correct.
운영 위생을 계약의 일부로 취급하십시오. 프로토콜 트래픽을 진단과 분리하고, 특히 stdout이 데이터이고 stderr가 로그인 stdio에서는 더욱 그렇습니다. 원격 HTTP 전송에서는 동일한 원칙이 세션 수명 주기, 재연결 동작, 그리고 재개 가능성에 적용됩니다. 이 요소들이 일관되지 않으면, 핸들러 자체가 올바르더라도 시스템을 이해하기 어려워집니다.
Add negative tests for malformed inputs, missing fields, downstream timeouts, and not‑found cases, then standardize error shape across the server so every tool does not invent its own private version of confusion. The important part is that typed failures survive translation through the MCP boundary instead of being collapsed into one generic error on the way out. MCP gives you the transport and result channel; the stable fields that make failures actionable still need to be part of your own server contract.
잘못된 입력, 누락된 필드, 하위 시스템 타임아웃, not‑found 경우에 대한 부정 테스트를 추가하고, 서버 전반에 걸쳐 오류 형태를 표준화하여 각 도구가 자체적인 혼란 버전을 만들지 않도록 하십시오. 중요한 점은 타입이 지정된 실패가 MCP 경계를 통과하면서 번역된 뒤에도 하나의 일반 오류로 축소되지 않아야 한다는 것입니다. MCP는 전송 및 결과 채널을 제공하지만, 실패를 실제로 조치할 수 있게 하는 안정적인 필드는 여전히 자체 서버 계약의 일부여야 합니다.
Lessons from the Trenches
이 패턴은 modelcontextprotocol/typescript-sdk #699에서 나타났으며, 실제 도구 예외가 오해를 일으키는 -32602 구조화된 콘텐츠 오류로 대체되었습니다. 시스템이 왜 실패했는지에 대해 거짓말을 시작하면, 모든 하위 디버깅 단계가 더 비싸게 됩니다. 모호한 오류는 더 이상 도움이 되는 신호가 아니라 책임이 됩니다.
Source: (source link remains unchanged)
Wrath
Wrath는 불확실성이나 실패에 대한 반응으로, 제어보다는 힘으로 표현됩니다. 코드를 보기 전에 디자인 대화에서 분노를 흔히 들을 수 있습니다:
- “실패하면 재시도한다.”
- “느리면 더 빨리 폴링한다.”
- “스트림이 끊어지면 즉시 재연결한다.”
이는 화를 내는 운영 버전이라고 할 수 있습니다.
How to spot it
- 재시도 루프나 재연결 폭풍이 장애 발생 시 로그에 나타납니다.
- 하나의 실패한 의존성이 갑자기 중복 요청, 중복 작업, 혹은 반복적인 서버 시작을 일으킬 수 있습니다.
- 클라이언트가 이미 성능 저하된 엔드포인트를 계속 두드립니다.
- Timeout graphs와 request‑volume graphs가 동시에 상승합니다.
Example
실제 장애 상황에서 내부 앱이나 어시스턴트가 MCP 연결을 잃고, 누군가가 이미 사고 대응 중일 때 발생합니다. 사용자는 단일 연결 끊김이나 너무 오래 걸리는 스피너를 경험합니다. 내부적으로는 인내심 없는 클라이언트가 그 하나의 중단을 반복적인 프로세스 시작, 중복 요청, 그리고 이미 실패하고 있는 시스템에 추가 부하를 일으키는 형태로 확대합니다.
Before
async function ensureConnection(client: McpClient, serverCommand: string) {
while (true) {
try {
await client.connect(new StdioTransport(serverCommand));
return;
} catch {
await sleep(100);
}
}
}After
function sleepWithAbort(ms: number, signal: AbortSignal) {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(timeout);
signal.removeEventListener("abort", onAbort);
reject(new Error("connection cancelled"));
};
if (signal.aborted) {
onAbort();
return;
}
signal.addEventListener("abort", onAbort, { once: true });
});
}
async function ensureConnection(
client: McpClient,
serverCommand: string,
abortSignal: AbortSignal
) {
for (let attempt = 1; attempt <= 5; attempt += 1) {
try {
if (abortSignal.aborted) throw new Error("connection cancelled");
await client.connect(new StdioTransport(serverCommand));
return;
} catch (error) {
if (abortSignal.aborted) throw error;
if (attempt === 5) throw error;
const backoffMs = attempt * 1000 + Math.floor(Math.random() * 250);
await sleepWithAbort(backoffMs, abortSignal);
}
}
}How to fix it
- 멈추는 법을 배우세요. 재시도에 엄격한 상한을 두고, 점진적인 백오프와 지터를 추가해 클라이언트가 동시에 재연결하지 않도록 합니다.
- 스레드 취소. 모든 outbound 요청 및 장기 실행 작업에 abort signal을 전파해 시스템이 무분별하게 확대되지 않고 중단될 수 있게 합니다.
- 재시도해도 안전한 작업을 결정하세요.
- 멱등적인 읽기와 재연결 시도는 일반적으로 안전합니다.
- 부수 효과가 있는 작업은 명시적인 멱등성 키나 다른 중복 방지 수단이 없으면 자동 재시도를 피해야 합니다.
- 재시도를 가시화하세요. 재시도 횟수, 백오프 지연, 재연결 폭풍을 계측하고 모니터링합니다. 측정하지 않으면 프로덕션에서 어려운 교훈을 얻을 때까지 문제를 발견하지 못합니다.
같은 경고가 관리형 엣지와 게이트웨이에도 적용됩니다. 스로틀링, 프록시 재시도, 정책 적용은 피해를 완화할 수 있지만, 비멱등적이거나 실패에 대해 모호하거나 반복해도 안전하지 않은 백엔드 작업을 해결해 주지는 못합니다.
Lessons from the Trenches
modelcontextprotocol/inspector #293– 연결 과정에서 서버가 반복적으로 시작되었습니다.modelcontextprotocol/inspector #723– 재연결 로직이 충분한 상태를 보존하지 못해 안전하게 재개하지 못했습니다.
교훈은 간단합니다: 재시도는 시스템 설계의 일부이며, 가장자리에서 얹는 임시 방편이 아닙니다. 재시도 정책이 충분히 설계되지 않으면…
explicit, you don’t really have one.
Why Operational Sins Are Hard to Fix
Operational sins usually demand shared infrastructure rather than isolated patches.
- Sloth fixes often mean building a validation layer, an error‑policy, structured logging, and a test harness for unhappy paths.
- Wrath fixes tend to reach across transport clients, job runners, background workers, and UI status handling. You may need:
- a retry helper,
- a back‑off policy,
- a cancellation model,
- idempotency protection, and
- dashboards that show retries and reconnects.
That work is easy to postpone because it doesn’t demo well. But once it exists, every future tool becomes cheaper to run, debug, and trust.