MCP 터널 문제 해결
MCP 터널 문제 해결 (Troubleshoot MCP tunnels)
터널을 통한 요청은 세 층 중 하나에서 실패할 수 있어요. 순서대로 진단해보세요. 터널 엣지로의 아웃바운드 연결, Anthropic에서 프록시로의 내부 TLS, 마지막으로 업스트림 MCP 서버 쪽의 라우팅과 IP 검증이에요.
출처: 문서
본문
참고 (Note) MCP 터널은 연구 프리뷰(research preview) 상태예요. 사용해보려면 액세스를 요청하세요.
터널을 통한 요청은 세 층 중 하나에서 실패할 수 있어요. 순서대로 진단해보세요. 터널 엣지로의 아웃바운드 연결, Anthropic에서 프록시로의 내부 TLS, 마지막으로 업스트림 MCP 서버 쪽의 라우팅과 IP 검증이에요.
빠른 참조 (Quick reference)
| 증상 | 원인 | 해결 방법 |
|---|---|---|
| 에이전트의 + MCP Server 선택기에 터널이 나타나지 않음 | 선택기는 세션의 워크스페이스에서 활성 인증서가 하나 이상 있는 터널만 나열해요. | CA 인증서를 등록하거나, 터널이 생성된 워크스페이스에서 세션을 여세요. |
호출자가 HTTP 500을 보는데 cloudflared 로그에 No ingress rules were defined |
cloudflared에 로컬 대상이 없어요. | cloudflared 서비스에 --url http://localhost:8080와 network_mode: \"service:mcp-proxy\"를 추가하세요. |
프록시 로그에 no route for host |
tunnel_domain이 할당된 도메인과 일치하지 않거나, config.yaml을 재시작 없이 편집했어요. |
tunnel_domain을 터널 상세 페이지에 표시된 정확한 도메인으로 설정하고 프록시를 재시작하세요 (docker compose restart mcp-proxy). |
프록시 로그에 IP validation failed: <ip> is not a private address |
업스트림 MCP 서버가 RFC1918 밖으로 해석돼요. | 업스트림 IP 검증을 참고하세요. |
프록시가 cannot unmarshal !!seq into map[string]string으로 종료 |
routes가 YAML 리스트예요. |
routes: { name: http://host:port }를 사용하세요. |
프록시가 open /data/tls.key: permission denied으로 종료 |
키가 0600인데 프록시 컨테이너가 비-root로 실행돼요. |
chmod 644 data/tls.key를 실행하세요. |
curl https://<proxy>:8080이 wrong version number로 실패 |
정상이에요. 리스너는 평문 WebSocket이에요. TLS는 WS 스트림 안에서 일어나요. | Managed Agent나 Messages API를 통해 검증하세요. |
아래 섹션에서는 한 줄로 고쳐지지 않는 실패를 다뤄요.
소스-IP 허용 목록 뒤에서의 OAuth 실패 (OAuth fails behind a source-IP allowlist)
OAuth 흐름은 인증 서버의 소스-IP 허용 목록이 Anthropic 백엔드가 /token, /register, 발견(discovery) 엔드포인트에 도달하는 것을 차단할 때 실패해요. Anthropic의 이그레스 범위를 허용 목록에 넣기 싫다면, 브라우저용 /authorize 엔드포인트는 기존 공용 호스트 이름에 두고 백엔드-백엔드 OAuth 호출만 터널을 통해 라우팅할 수 있어요.
-
인증 서버용 프록시 라우트 추가하기
routes: mcp: http://your-mcp-server:8080 auth: http://your-auth-server:8080routes를 편집한 후 프록시를 재시작하세요 (docker compose restart mcp-proxy, 또는helm upgrade). -
분할 엔드포인트 발견 메타데이터 제공하기 — 인증 서버의
/.well-known/oauth-authorization-server응답은authorization_endpoint를 기존 허용 목록 호스트 이름으로 가리키고, 나머지는 터널을 가리켜야 해요:{ "issuer": "https://auth.<tunnel-domain>", "authorization_endpoint": "https://<your-allowlisted-host>/authorize", "token_endpoint": "https://auth.<tunnel-domain>/token", "registration_endpoint": "https://auth.<tunnel-domain>/register", "code_challenge_methods_supported": ["S256"] } -
MCP 서버를 터널 발급자로 지정하기 — MCP 서버의
/.well-known/oauth-protected-resource응답은 인증 서버로 터널 호스트 이름을 참조해야 해요:{ "resource": "https://mcp.<tunnel-domain>", "authorization_servers": ["https://auth.<tunnel-domain>"] }
이 구성을 사용하면 사용자의 브라우저는 기존 호스트 이름의 /authorize(허용 목록이 이미 허용하는 곳)를 누르고, Anthropic 백엔드는 터널을 통해 /token, /register, 발견 문서에 도달해요.
설정 구성 요소 인증 실패 (Setup component authentication failures)
설정 구성 요소(Helm Job 또는 Compose setup 서비스)는 페더레이션 규칙을 통해 OIDC JWT를 교환해서 Tunnels API에 인증해요. 교환에 실패하면 Workload Identity Federation 레퍼런스의 실패한 교환 문제 해결을 참고하세요. 실패 모드(subject, audience, issuer, JWKS, lifetime)가 동일해요.
터널 특유의 원인:
- 차트의 기본 audience는
api.anthropic.com(스킴 없음)이에요. 규칙의 audience가https://api.anthropic.com이라면api.wif.audience를 일치하도록 설정하세요. - 성공적인 교환 후 Tunnels API가
403을 반환하면 규칙의 스코프에workspace:manage_tunnels가 없거나, 규칙의 서비스 계정이 터널의 워크스페이스 멤버가 아니라는 뜻이에요. 스코프를 설정하고 서비스 계정을 워크스페이스에 추가하세요.
Helm에서는 설정 구성 요소가 사전 설치 훅 Job으로 실행돼요. 실패하면 Job이 검사용으로 남아요 (kubectl logs job/mcp-tunnel-setup -n mcp-tunnel). Helm은 훅 리소스를 관리하지 않으므로, 재시도 전에 삭제하세요:
helm uninstall mcp-tunnel -n mcp-tunnel
kubectl -n mcp-tunnel delete job mcp-tunnel-setup
터널이 연결되지 않아요 (Tunnel won't connect)
먼저 cloudflared 로그를 확인하세요. 일반적인 원인:
TUNNEL_TOKEN이 없거나, 만료되었거나, 잘못 복사됐어요.- 방화벽이 터널 엣지로의 포트 7844 아웃바운드 TCP/UDP를 차단하고 있어요.
cloudflared는 UDP 수신 버퍼 크기에 대한 경고를 로그로 남길 수도 있는데, 이는 QUIC 튜닝 힌트이지 오류가 아니에요.
인증서 오류 (Certificate errors)
내부 TLS 동안 Anthropic이 프록시의 인증서를 거부하면 프록시는 tls handshake failed를 로그로 남겨요. 다음을 확인하세요:
- 서버 인증서가 만료되지 않았는지.
- 인증서의 Subject Alternative Name이
*.<tunnel-domain>과 일치하는지. - 서명 CA가 이 터널에 대해 Anthropic에 등록되어 있는지.
전체 검증 규칙은 인증서 요구 사항을 참고하세요.
업스트림 IP 검증 (Upstream IP validation)
SSRF 보호를 위해 프록시는 기본적으로 RFC1918 사설 범위(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)의 주소만 연결해요. 프록시-업스트림 연결은 IPv4만 지원해요. (네트워크 요구 사항의 cloudflared-엣지 이그레스 범위는 다른 홉이에요.)
프록시가 IP validation failed: <ip> is not a private address를 로그로 남기면 업스트림 호스트 이름이 이 집합 밖으로 해석된 거예요. Kubernetes에서는 일부 관리형 배포가 Service CIDR을 RFC1918 밖에 할당해요. kubectl get svc kubernetes -n default -o jsonpath='{.spec.clusterIP}'가 사설 범위 밖의 주소를 반환한다면 클러스터의 Service CIDR을 찾아서 추가하세요.
주소가 정당하다면 upstream.allowed_ips에 가장 좁은 범위의 CIDR을 추가하세요. allowed_ips를 설정하면 RFC1918 기본값을 확장하는 대신 대체하므로, 다른 업스트림 MCP 서버가 사용하는 사설 범위도 포함하세요:
upstream:
allowed_ips:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 127.0.0.0/8 # loopback, for local testing only
경고 (Warning) 로컬 테스트 밖에서는
0.0.0.0/0을 피하세요. SSRF 보호를 완전히 꺼버려요.
더 알아보기 (Learn more)
- MCP 터널 레퍼런스 — 인증서 검증 규칙
- MCP 터널 개요 — 터널링된 MCP 서버 사용하기
- Workload Identity Federation 문제 해결 — 실패한 교환 진단