제 MCP 서버는 그냥 작동을 멈추곤 했습니다. 크래시 덤프도 없었고, 로그에 스택 트레이스도 남지 않았습니다. 클라이언트들은 아무런 불만 없이 연결되어 있다가, 몇 시간 뒤에 서버 전체가 침묵에 빠졌습니다. 요청은 사라졌고, 반대편의 AI 에이전트는 아무런 응답도 받지 못한 채 빈 공백만을 마주했습니다.

이는 Model Context Protocol (MCP) 생태계에서 매우 빈번하게 발생하는 답답한 문제입니다. 이 프로토콜은 AI 에이전트가 외부 도구를 발견하고 호출하는 방식을 정의하지만, 명세서는 에러 처리를 사용자가 직접 수행할 것을 가정합니다. 대부분의 튜토리얼과 초기 구현 예제들은 이 부분을 건너뜁니다. 그들은 정상적인 흐름(happy path), 즉 함수에 어노테이션을 달고, 서버를 통해 노출하며, 깔끔한 결과를 반환하는 것에만 집중합니다. 외부 API에 네트워크 일시 오류가 발생하거나, 모델이 파라미터 이름을 환각(hallucinate)하여 잘못된 입력을 보낼 때 어떤 일이 벌어지는지는 거의 보여주지 않습니다. 그 결과, 겉보기에는 건강해 보이지만 실제로는 몇 시간째 죽어 있는 취약한 서버가 만들어집니다.

왜 빈 응답이 크래시보다 더 나쁜가

MCP 도구 핸들러에서 처리되지 않은 예외(unhandled exception)가 발생하면, 전송 계층(transport layer)이 이를 삼켜버리는 경우가 많습니다. 서버 프로세스는 살아 있고 소켓도 열려 있지만, 클라이언트는 빈 응답을 받게 됩니다. 이는 모니터링 시스템이 감지하지 못할 수 있기 때문에 명시적인 크래시보다 더 위험합니다. 프로세스는 여전히 실행 중이고 포트도 여전히 리스닝 상태이지만, 모든 도구 호출이 아무것도 반환하지 않기 때문입니다.

AI 모델은 침묵을 실패로 해석하지 않습니다. 대신 데이터를 생성하지 않은 성공적인 호출로 해석합니다. 이러한 빈 응답은 모델이 임기응변을 하도록 학습시킵니다. 모델은 빈 공간을 채우기 위해 사실을 환각하기 시작하거나, 동일한 잘못된 호출을 반복하는 루프에 빠집니다. 일시적인 네트워크 타임아웃이나 잘못된 도구 인자 같은 사소한 문제가 이런 식의 동작을 유발하도록 방치해서는 안 됩니다.

래퍼 패턴: 3단계 방어선

저는 모든 도구 핸들러를 얇은 에러 복구 계층으로 감싸는 방식으로 이 문제를 해결했습니다. 이 래퍼(wrapper)는 발생 가능한 모든 실패를 예측하려 하지 않습니다. 대신 에러를 분류하고 그에 따라 적절히 응답합니다.

ConnectionError and TimeoutError
이는 서버가 외부 API와 통신할 때 네트워크가 불안정할 경우 발생합니다. 본능적인 해결책은 MCP 서버 프로세스 전체를 재시작하는 것이겠지만, 그렇게 하지 마십시오. 재시작을 하면 활성화된 클라이언트 연결이 끊기고, 메모리 내의 모든 상태가 삭제되며, 전체 재초기화가 강제됩니다. 대신, 연결 실패를 포착하여 도구가 사용하는 전송 계층이나 HTTP 클라이언트만 다시 연결하십시오. 그러면 서버는 따뜻한 상태(warm state)를 유지하며 즉시 다음 요청을 처리할 준비를 할 수 있습니다.

ValueError
AI 클라이언트가 잘못된 형식의 인자를 보낼 때 발생하는 에러입니다. 모델이 존재하지 않는 파라미터를 만들어냈거나, 정수가 필요한 곳에 문자열을 전달했거나, 필수 필드를 누락했을 수 있습니다. 이 에러를 처리하지 않고 그대로 방치하면 클라이언트는 크래시를 겪거나 빈 응답을 받게 됩니다. 래퍼 내부에서 이를 포착한 다음, 모델에게 무엇이 잘못되었는지 정확히 알려주는 명확하고 구체적인 메시지를 구성하십시오. 어떤 파라미터가 실패했는지, 무엇이 기대되었는지를 설명하십시오. 대부분의 현대적인 AI 모델은 그 메시지를 읽고 바로 다음 턴에서 스스로 수정합니다. 모호한 에러는 추론 사이클을 낭비하지만, 정확한 에러는 즉시 문제를 해결합니다.

General Exceptions
안전망을 유지하십시오. 에러가 위의 카테고리에 해당하지 않는 경우, 개발자를 위해 상세 내용을 로그로 남기고 클라이언트에는 깔끔하고 일반적인 실패 응답을 반환하십시오. 이렇게 하면 하나의 기이한 예외 케이스가 모든 사용자의 세션을 종료시키는 것을 방지할 수 있습니다. 서버는 생존하고, 클라이언트는 무언가 실패했다는 신호를 받으며, 여러분은 나중에 디버깅할 수 있는 충분한 컨텍스트를 로그에 남길 수 있습니다.

isError 플래그는 타협할 수 없는 필수 사항입니다

여러분의 해결책이 실제로 작동할지를 결정하는 핵심적인 디테일이 여기 있습니다. MCP 응답에는 isError라는 불리언(boolean) 필드가 포함되어 있습니다. 예외가 발생했을 때 isErrortrue로 설정하지 않고 에러 메시지만 반환하면, 클라이언트는 해당 에러 텍스트를 성공적인 도구 결과로 취급합니다.

외부 API가 요청 제한(rate limit)에 걸렸다고 가정해 봅시다. 예외를 포착하여 "API rate limit exceeded"라는 문자열을 반환하면서 isErrorfalse로 남겨두었습니다. 클라이언트는 이 문자열을 마치 실제 도구 출력값인 것처럼 모델의 컨텍스트 윈도우(context window)에 전달합니다. 그러면 모델은 그 텍스트를 데이터인 것처럼 간주하고 추론을 시도합니다. 요약 내용에 에러 메시지를 인용하거나, 더 심하게는 그 에러 텍스트와 다른 사실들 사이의 관계를 환각해낼 수도 있습니다. 일시적인 인프라의 결함을 잘못된 정보의 근원으로 만들어 버린 것입니다.

에러 페이로드를 반환할 때는 항상 isErrortrue로 설정하세요. 이렇게 하면 클라이언트에 도구 호출(tool call)이 실패했다는 명확한 신호를 줄 수 있으며, 모델이 재시도할지, 설명을 요청할지, 아니면 완전히 다른 도구를 시도할지 결정할 수 있게 합니다.

무엇을 잡고(Catch) 무엇을 종료할지(Kill) 결정하세요

모든 것을 삼켜버리는 무분별한 try-catch로 서버 전체를 감싸지 마세요. 어떤 에러는 서버가 즉시 중단되어야 함을 의미합니다. 시작 시 필수 환경 변수가 누락되었거나 설정 파일이 손상된 경우, 요청 수준에서 아무리 에러를 잡아내려 해도 소용이 없습니다. 이러한 치명적인 에러를 위한 별도의 예외 클래스를 생성하고, 프로세스가 종료되도록 두세요.

규칙은 간단합니다. 에러가 일시적이거나 단일 요청에 국한된 것이라면, 이를 잡아내어 복구하세요. 만약 에러로 인해 이후의 모든 요청이 실패할 것이 확실하다면, 서버가 확실하게 종료되도록 하세요. 시작 단계에서 빠르게 실패하는 것이, 고장 난 상태로 며칠 동안 간신히 버티는 서버보다 훨씬 낫습니다.

필요해지기 전에 관측성(Observability)을 확보하세요

래퍼(wrapper)를 구현했다면, 이를 구조화된 로깅(structured logging)과 결합하세요. 모든 도구 호출과 그 결과를 JSON 형식으로 기록하세요. 도구 이름, 원시 인자(raw arguments), 지연 시간(latency), 그리고 성공, 실패 또는 재시도 여부를 포함해야 합니다.

이러한 습관은 곧 빛을 발합니다. 에러가 급증하는 것을 발견했을 때, 도구별로 필터링하여 몇 분 만에 패턴을 찾아낼 수 있습니다. 예를 들어, 특정 외부 API가 매일 같은 시간에 타임아웃을 발생시킨다면, 미처 몰랐던 정기 점검 시간을 찾아낼 수 있습니다. 혹은 특정 도구가 지속적으로 잘못된 형식의 인자를 받는다면, 상위 단계의 프롬프트 엔지니어링 결함을 발견할 수도 있습니다. 스택 트레이스(stack trace) 속에 파묻힌 일반 텍스트 로그는 이러한 추적 작업을 고통스럽게 만들지만, 구조화된 JSON은 이를 매우 쉽게 만들어 줍니다.

프로덕션 적용 결과

지난 3주 동안 두 개의 프로덕션 MCP 서버에 이 래퍼 패턴을 적용해 왔습니다. 그동안 단 한 건의 '조용한 실패(silent failure)'도 발생하지 않았습니다. 래퍼를 추가하기 전에는 매일 평균 한 번꼴로 원인을 알 수 없는 실패가 발생했습니다. 이 패턴은 복잡하지 않지만, 견딜 수 있는 노이즈와 실제 문제를 분리해 주기 때문에 그 효과는 매우 강력합니다.

조용한 실패는 크래시(crash)보다 더 큰 비용을 초래합니다. 크래시는 알림 시스템을 작동시키지만, 침묵은 신뢰를 갉아먹습니다. 어느 날은 AI 에이전트가 유용한 도구 데이터를 반환하다가, 다음 날에는 서버가 몇 시간 전에 응답을 멈췄다는 이유로 엉뚱한 말을 지어내기 시작할 수 있습니다. 래퍼 패턴은 그 간극을 메워줍니다. 사소한 난기류 속에서도 서버를 계속 작동하게 유지하고, 모델이 스스로 실수를 바로잡을 수 있도록 충분한 컨텍스트를 제공하며, 정말 치명적인 문제가 발생했을 때는 즉시 알 수 있도록 보장합니다.

지금 MCP 도구를 만들고 있다면, 래퍼와 isError 플래그부터 시작하세요. 그 외의 것들은 모두 사후 정리 작업일 뿐입니다.