Published on

Claude MCP 연동 시 401·403·툴 미호출 원인과 해결

Authors

서론

Claude에 MCP(Model Context Protocol) 서버를 붙이면 “모델이 도구를 알아서 호출한다”는 기대가 생깁니다. 그런데 실제 현장에서는 아래 3가지가 가장 흔하게 발목을 잡습니다.

  • 401 Unauthorized: 토큰/헤더/서명/세션 문제로 인증 단계에서 실패
  • 403 Forbidden: 권한(IAM/스코프/리소스 정책) 또는 네트워크 경계에서 차단
  • 툴 미호출(tool not invoked): MCP 서버는 살아있고 도구 목록도 보이는데, Claude가 도구를 호출하지 않음

이 글은 “무조건 재시도/재배포”가 아니라, 요청 흐름을 쪼개서 어디서 끊겼는지를 확인하고, 재현 가능한 체크리스트로 해결하는 것을 목표로 합니다.

> EKS 같은 클러스터에서 돌리는 MCP 서버라면 네트워크/egress가 인증 오류처럼 보이는 경우도 많습니다. egress 자체가 막혀 외부 API 호출이 실패하는 케이스는 EKS에서 Pod는 정상인데 egress만 막힐 때 점검도 함께 참고하세요.


전체 아키텍처와 실패 지점 분해

MCP 연동을 단순화하면 보통 아래 흐름입니다.

  1. Claude(클라이언트) → 2. MCP 서버(툴 라우터) → 3. 실제 외부 시스템(API/DB/SaaS)

오류를 3단계로 분리하면 원인도 명확해집니다.

  • 401/403이 MCP 서버 앞단에서 발생: Claude→MCP 인증/인가 문제
  • 401/403이 외부 시스템 호출에서 발생: MCP→외부 API 인증/인가 문제
  • 툴 미호출: 1단계(모델의 도구 선택)에서 실패하거나, 2단계(도구 메타데이터) 품질 문제

이제 각 증상을 케이스별로 진단합니다.


401 Unauthorized: “토큰이 맞는데 왜?”를 끝내는 체크리스트

401은 보통 “인증 정보가 없거나, 형식이 다르거나, 만료”입니다. 하지만 MCP에서는 헤더 전달/프록시/서버 구현에서 자주 꼬입니다.

1) Authorization 헤더 형식과 전달 여부 확인

가장 먼저 할 일은 MCP 서버가 실제로 받은 헤더를 로그로 남기는 것입니다.

Node.js(Express) 예시: 들어온 헤더 덤프

import express from "express";

const app = express();

app.use((req, res, next) => {
  console.log("[REQ]", req.method, req.url);
  console.log("[HEADERS]", JSON.stringify(req.headers, null, 2));
  next();
});

app.get("/health", (req, res) => res.json({ ok: true }));

app.listen(8080, () => console.log("listening 8080"));

여기서 확인할 포인트:

  • authorization 헤더가 아예 없는지
  • 값이 Bearer <token> 형태인지(공백/대소문자/접두어)
  • 프록시(Nginx/ALB/Ingress)가 Authorization을 드랍하지 않는지

Kubernetes Ingress/프록시를 쓰는 경우, Authorization 헤더가 누락되면 클라이언트는 토큰을 보냈다고 믿는데 서버는 못 받는 상황이 됩니다.

2) 토큰 만료/시계 오차(clock skew)

JWT나 만료가 있는 토큰이면, 컨테이너/노드의 시간이 틀어져도 401이 납니다.

  • 노드 NTP 동기화
  • 토큰 발급 서버와 수신 서버의 시간 차

3) 잘못된 audience/issuer/scope

OAuth/JWT 기반이면 “서명이 맞다”와 “이 토큰을 이 서비스가 받아도 된다”는 별개입니다.

  • aud(audience)가 MCP 서버(또는 API Gateway)에 맞는지
  • iss(issuer)가 기대 값인지
  • 필요한 scope가 있는지

4) 로컬에서는 되는데 EKS에서만 401?

이 경우 401이지만 실은 외부 인증 서버로의 통신 실패가 원인일 수 있습니다(인증 미들웨어가 introspection 호출 실패 → 401로 변환).

  • NAT/라우팅/보안그룹/NetworkPolicy로 egress 차단

EKS에서 “Pod는 뜨는데 밖으로 못 나간다”는 전형적인 패턴은 EKS에서 Pod는 정상인데 egress만 막힐 때 점검을 따라가면 빠르게 좁힐 수 있습니다.


403 Forbidden: 인증은 됐는데 ‘권한’이 없다

403은 “너 누구인지는 알겠는데, 이 작업은 안 돼”입니다. MCP 연동에서는 특히 클라우드 권한(IAM), 리소스 정책, 엔드포인트 정책에서 많이 발생합니다.

1) MCP 서버가 호출하는 외부 API에서 403이 나는지부터 확인

중요한 건 403의 주체입니다.

  • Claude→MCP가 403: MCP 서버 앞단 권한 정책
  • MCP→외부 API가 403: 외부 API 권한 정책

MCP 서버에서 외부 호출 직전에 다음을 로깅하세요.

  • 호출 URL(민감 파라미터 제외)
  • HTTP 메서드
  • 사용한 자격증명 타입(예: IAM Role, API Key, OAuth token)
  • 외부 응답 바디(가능한 범위)

2) AWS에서 “Pod는 되는데 SQS만 403” 같은 케이스

EKS에서 특정 AWS 서비스만 403이 뜨면, 네트워크보다 IAM/정책일 가능성이 큽니다.

  • IRSA(ServiceAccount Role) 적용 여부
  • AssumeRole이 실제로 되는지
  • 리소스 정책(SQS queue policy, KMS key policy 등)에서 principal이 허용되는지

이 패턴은 EKS에서 Pod는 되는데 SQS만 403 뜰 때와 진단 흐름이 거의 동일합니다.

3) WAF/ALB/Ingress가 403을 반환하는 경우

애플리케이션이 아니라 인프라가 403을 만들면, 서버 로그에는 아무 것도 안 남고 클라이언트만 403을 봅니다.

  • ALB/WAF 규칙에 의해 차단(특정 User-Agent, Body 패턴, IP/Geo)
  • Ingress 인증 플러그인(예: oauth2-proxy)에서 차단

이때는 ALB/WAF/Ingress 액세스 로그를 반드시 확인해야 합니다.


툴 미호출(tool not invoked): “도구는 등록됐는데 왜 안 써?”

가장 답답한 케이스가 이겁니다. 서버는 살아있고 도구 목록도 보이는데, Claude가 계속 텍스트로만 답하고 도구를 호출하지 않습니다.

툴 미호출은 대개 아래 4가지로 좁혀집니다.

  1. 도구가 필요하다는 신호가 프롬프트에 없음
  2. 도구 스키마/설명이 모델에게 불리하게 작성됨
  3. 도구 호출이 실패했는데 클라이언트가 숨김(또는 재시도 로직이 이상)
  4. 모델/클라이언트 설정에서 tool 사용이 제한됨

1) “도구를 써야 한다”를 모델이 판단할 수 있게 만들기

모델은 기본적으로 “굳이 도구를 안 써도 되는” 방향으로 답을 만들 수 있습니다. 특히 다음 상황에서 툴 호출 확률이 떨어집니다.

  • 질문이 추상적(예: “요약해줘”, “설명해줘”)
  • 도구를 써야만 알 수 있는 정보가 없거나, 있어도 사용자 요구가 약함
  • 도구 호출 비용/시간이 큰 것으로 학습된 듯한 상황(불확실)

해결: 프롬프트에 명시적 성공 조건을 넣기

예: “최신 데이터 조회가 필요하면 반드시 tool을 호출” 같은 규칙을 시스템/개발자 메시지에 둡니다.

규칙:
- 사용자의 질문이 외부 시스템 조회(예: DB, HTTP API)가 필요하면 반드시 MCP tool을 호출한다.
- tool 호출 없이 추측으로 답하지 않는다.
- tool 결과에 근거하여 답하고, 근거 데이터(요약)를 함께 제공한다.

2) 도구 설명(description)과 입력 스키마가 모델 친화적인지

툴 메타데이터는 모델이 “이 도구가 언제 필요한지”를 판단하는 유일한 힌트입니다.

  • description이 너무 짧거나 모호하면 선택되지 않음
  • 입력 스키마가 과도하게 엄격/복잡하면 회피됨
  • 필수 파라미터가 많으면 호출 실패 가능성이 커져 회피됨

예: 나쁜 도구 설명 vs 좋은 도구 설명

  • 나쁨: "fetch" / "get data"
  • 좋음: "주문번호(orderId)로 주문 상태/배송 상태를 조회한다. 사용자가 주문 상태를 묻는 경우 항상 사용."

3) 도구 호출 실패가 “미호출처럼” 보이는 경우

클라이언트가 tool 호출을 시도했지만,

  • 타임아웃
  • 401/403
  • 5xx

등으로 실패하면, UI/SDK가 이를 숨기고 모델이 텍스트로 대체 응답을 내놓는 경우가 있습니다.

해결: MCP 서버에 요청 단위 상관관계 ID를 심기

  • x-request-id를 받아 로그에 남기고
  • tool 실행 시작/종료/에러를 구조화 로그(JSON)로 기록
function logEvent(type, payload) {
  console.log(JSON.stringify({
    ts: new Date().toISOString(),
    type,
    ...payload,
  }));
}

app.post("/mcp/tool", async (req, res) => {
  const rid = req.headers["x-request-id"] || crypto.randomUUID();
  logEvent("tool.start", { rid, tool: req.body?.tool });

  try {
    // 실제 실행
    const result = await runTool(req.body);
    logEvent("tool.success", { rid });
    res.json({ rid, result });
  } catch (e) {
    logEvent("tool.error", { rid, message: e.message });
    res.status(500).json({ rid, error: e.message });
  }
});

이렇게 하면 “정말로 호출이 안 된 것인지” vs “호출했는데 실패한 것인지”가 분리됩니다.

4) 모델/클라이언트 설정에서 tool 사용이 꺼져 있지 않은지

환경에 따라 다음이 tool 호출을 막습니다.

  • 클라이언트 SDK에서 tool 사용 옵션이 비활성
  • 특정 모델/모드에서 tool 호출 제한
  • 정책상 외부 호출 금지(엔터프라이즈 환경)

체크리스트:

  • 현재 세션에 tools가 실제로 attach 되었는지
  • tool choice가 auto인지, 혹은 강제로 none이 아닌지
  • 응답에 tool 관련 중간 이벤트가 오는지(스트리밍 이벤트 포함)

재현 가능한 진단 루틴(10분 컷)

현장에서 빠르게 원인을 좁히려면 아래 순서가 효율적입니다.

1) curl로 MCP 서버를 직접 때리기

Claude를 거치지 말고, 동일한 인증 헤더로 MCP 서버 엔드포인트를 직접 호출합니다.

curl -i https://mcp.example.com/health

curl -i https://mcp.example.com/tools \
  -H 'Authorization: Bearer YOUR_TOKEN'
  • 여기서 401이면 Claude 이전에 이미 인증이 깨진 것
  • 여기서 200인데 Claude에서만 실패하면, Claude 클라이언트 설정/프록시/헤더 전달 문제

2) MCP 서버에서 외부 API 호출을 분리 테스트

MCP 서버 내부에서 외부 API를 호출하는 코드가 있다면, 그 호출을 독립적으로 테스트합니다.

# (예) Pod 내부에서 외부 API 접근 확인
kubectl exec -it deploy/mcp -- sh

# DNS
nslookup api.vendor.com

# TLS/HTTP
curl -v https://api.vendor.com/v1/ping
  • DNS 실패/timeout이면 egress/라우팅 문제
  • 403이면 자격증명/권한 문제

3) 403이 ‘인프라’인지 ‘애플리케이션’인지 구분

  • ALB/WAF 로그에 403이 찍히는지
  • MCP 애플리케이션 로그에 요청이 도달했는지

ALB/Ingress 계층 이슈는 502/503과 함께 묶여 나타나기도 합니다. 유사한 네트워크/타겟 상태 진단은 EKS ALB Ingress 502 Target reset 원인과 해결도 참고하면 좋습니다.


운영에서 자주 놓치는 ‘정책/보안’ 포인트

1) 토큰을 로그에 남기지 말고, 해시/마스킹

디버깅하려고 Authorization 전체를 찍으면 사고가 납니다.

  • 앞 6자/뒤 4자만 남기기
  • 또는 SHA-256 해시만 남기기

2) 실패 응답 바디를 표준화

MCP 서버가 401/403을 반환할 때, 원인 코드를 일관되게 내려주면 클라이언트/관제에서 빨리 분류됩니다.

예:

{ "error": "UNAUTHORIZED", "reason": "MISSING_AUTH_HEADER" }
{ "error": "FORBIDDEN", "reason": "INSUFFICIENT_SCOPE", "required": ["tools:execute"] }

3) 리트라이가 401/403을 악화시키는 경우

  • 401/403은 보통 재시도해도 해결되지 않습니다.
  • 무한 재시도는 WAF 차단/레이트리밋을 유발해 더 큰 장애로 번집니다.

권장:

  • 401/403은 즉시 실패 처리
  • 429/5xx만 제한적 재시도

결론

Claude MCP 연동에서 401/403/툴 미호출은 “각각 다른 문제”처럼 보이지만, 실제로는 요청 흐름(Claude→MCP→외부 API) 중 어디에서 실패했는지 분리하면 대부분 빠르게 해결됩니다.

  • 401: 헤더 전달/토큰 형식/만료/clock skew/인증 서버 접근성부터 확인
  • 403: 인증 성공 이후의 권한 문제(IAM/리소스 정책/WAF/엔드포인트 정책)로 좁히기
  • 툴 미호출: 프롬프트의 성공 조건, 도구 description/스키마 품질, “호출했지만 실패한” 케이스를 로그로 분리

특히 Kubernetes/EKS 환경에서는 egress/인프라 정책 문제가 인증 문제처럼 위장되는 일이 잦습니다. 위 체크리스트대로 curl로 직접 확인 → 서버 로그 상관관계 ID로 추적 → 인프라 로그로 경계 구분만 해도, 재현 가능한 방식으로 원인을 잡을 수 있습니다.