튜토리얼
MCP 클라이언트가 OAuth issuer를 고정하는 방법: Python SDK 1.30과 2.2로 올린 뒤에도 남는 일
서버의 OAuth는 누가 도구를 호출할 수 있는지를 정합니다. 9월 28일 안내가 묻는 것은 클라이언트입니다. 발견 결과가 가리킨 로그인 대상을 그대로 믿을 것인가.
2026년 9월 28일, 공식 MCP Python SDK 유지관리자는 결함이 있는 HTTP 클라이언트가 실제 로그인 대상에 전달해야 할 client secret, authorization code, PKCE 증명을 상대가 통제하는 token 엔드포인트로 보낼 수 있다고 확인했습니다. 수정 버전은 1.x의 1.30.0과 2.x의 2.2.0입니다. 9월 7일에 나왔고, 당시 릴리스 노트는 동작 변경으로 적었습니다. 안내 자체는 9월 28일이고, Cycode도 같은 날 연구를 공개했으며 다음 날 여러 보안 매체가 전했습니다. 경과는 The Hacker News 기사, 지금 올바른 클라이언트 형태는 OAuth clients입니다. 서버가 토큰을 요구하는 방법은 사이트의 MCP OAuth 2.1 글입니다. 직접 완전히 통제하지 않는 원격에 연결할 때의 배치는 Remote MCP를 프로덕션에를 보십시오.
안내가 고치는 것은 클라이언트이고, 이미 보호한 서버가 아니다
영향을 받는 것은 공식 Python SDK를 HTTP 클라이언트로 쓰고 OAuth를 켠 프로세스입니다. 완전히 신뢰하지 않는 MCP 서버에 연결하면서 실제 로그인 서비스의 자격 증명을 쥐고 있습니다. 유지관리자 설명은, 일부 발견 경로가 기대하는 로그인 대상을 정하기 전에 서버가 가리킨 쪽을 받아들였다는 것입니다. 수정 버전은 기대하는 issuer를 먼저 정한 다음 인가 서버 메타데이터를 가져옵니다. 문서의 issuer가 그 대상이 아니면 거부합니다. 저장한 자격 증명에도 로그인 대상이 붙고, A로 표시된 기록은 B와 토큰을 바꾸지 않습니다.
이것은 「MCP 서버에 OAuth 2.1을 다는」 층과 다릅니다. 서버는 누가 도구를 호출할 수 있는지, 토큰이 이 리소스용인지를 정합니다. 클라이언트는 발견 문서가 가리키는 인가 서버가 client secret을 받은 그 서버인지를 정합니다. 둘 다 필요합니다. 서버만 보호하면 클라이언트는 발견 도중에 다른 대상을 따라갈 수 있습니다. 클라이언트만 고쳐도 토큰 대상을 검사하지 않는 서버의 다른 문제는 가리지 못합니다.
본선에 들어간 수정의 설명은 PR 3398입니다. 인가 서버 메타데이터는 가져온 그 서버를 issuer로 자칭해야 합니다. 근거는 RFC 8414 3.3절입니다. 맞지 않으면 멈춥니다. 클라이언트에 「issuer를 무시」하는 스위치를 넣지 마십시오.
| 돌리는 것 | 이 안내 | 올린 뒤에도 할 일 |
|---|---|---|
| HTTP 클라이언트, OAuthClientProvider | 1.9.1–1.29.1 또는 2.0.0–2.1.1 | 1.30.0 또는 2.2.0으로 올리고 옛 client_info를 삭제 |
| 클라이언트 자격 증명 또는 개인키 JWT | 같은 버전 범위 | 업그레이드와 함께 issuer를 전달 |
| SDK로 만든 서버, stdio, 자체 토큰 | 이 안내의 영향 밖 | 의존성은 수정 버전으로 올린다 |
영향 범위에 있는 쪽을 먼저 가른다
1.x는 1.9.1부터 1.29.1, 2.x는 2.0.0부터 2.1.1입니다. 클래스는 OAuthClientProvider, ClientCredentialsOAuthProvider, PrivateKeyJWTOAuthProvider, 그리고 issuer 인자가 없는 폐기된 RFC7523OAuthClientProvider입니다. Cycode는 높음으로 평가했습니다. 로컬 stdio, 직접 토큰을 붙이는 클라이언트, SDK로 서버만 작성하는 배포는 이 안내의 영향 밖입니다.
범위가 좁다고 옛 pin을 남겨도 되는 것은 아닙니다. CLI, 야간 작업, 데스크톱 클라이언트가 같은 잠금 파일의 mcp를 공유하는 경우가 많습니다. 그림에는 서버만 SDK를 쓴다고 되어 있어도 HTTP 클라이언트가 1.29로 설치되어 있습니다. 올리는 기준은 잠금 파일의 실제 버전입니다.
옛 버전에서 안내가 주는 임시 조치는 하나입니다. OAuth를 켠 클라이언트는 신뢰하는 서버에만 연결합니다. 이미 신뢰하지 않는 서버에 연결했다면 로그인 대상에서 client secret을 교체하고 발급된 토큰을 취소합니다. 이 단계는 환경 밖으로 나갔을 수 있는 자격 증명에 대한 것이고, 발견 과정을 재현하라는 뜻이 아닙니다.
1.30 또는 2.2 이후에도 머신 간에는 issuer가 필요하다
브라우저 흐름인 OAuthClientProvider는 수정 버전에서 기대하는 로그인 대상을 정한 뒤 메타데이터를 가져옵니다. 브라우저가 없는 두 경로는 다릅니다. ClientCredentialsOAuthProvider와 PrivateKeyJWTOAuthProvider에는 issuer를 넘깁니다. 값은 그 인가 서버의 /.well-known/oauth-authorization-server 문서가 선언한 issuer입니다. 발견은 그대로 돌지만 토큰 교환은 그 대상의 메타데이터만 씁니다. 서버가 다른 곳을 가리키면 OAuthFlowError로 멈춥니다.
issuer를 넘기지 않으면 이 두 경로는 수정 버전에서도 발견 결과를 따라갑니다. 안내는 분명합니다. 이 인자가 없으면 업그레이드는 머신 간에 닿지 않습니다. 1.30.0의 알림은 일반 DeprecationWarning이고 Python은 기본적으로 숨기므로 CI 로그에 안 보이는 경우가 많습니다. 3.0에서는 필수가 됩니다. 지금 적으십시오. 메이저에서 작업이 붉어지기를 기다리지 마십시오.
RFC7523OAuthClientProvider에는 issuer 인자가 없습니다. 계속 쓰면 올려도 로그인 대상을 고정할 수 없습니다. ClientCredentialsOAuthProvider나 PrivateKeyJWTOAuthProvider로 옮긴 다음 issuer를 넘깁니다. 비밀은 환경 변수나 비밀 관리자에서 읽고 저장소에 적지 않습니다.
신뢰하는 메타데이터에서 issuer는 가져온 주소와 같아야 한다
이미 신뢰하는 인가 서버에서 문서를 가져옵니다. 예는 https://auth.example.com/.well-known/oauth-authorization-server입니다. 안의 issuer는 그 서버 자신이어야 합니다. 비교는 문자열 일치이고 정규화하지 않습니다. 끝 슬래시, 호스트 대소문자, www 접두사는 다른 값입니다. 클라이언트에 넘기는 issuer는 문서의 원문입니다. 비슷해 보이는 주소를 손으로 치지 마십시오.
아래 JSON은 신뢰하는 인가 서버의 메타데이터입니다. issuer는 가져온 오리진과 같고, token 엔드포인트도 같은 호스트에 있습니다.
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
"code_challenge_methods_supported": ["S256"],
"authorization_response_iss_parameter_supported": true
}
Protected Resource Metadata는 어느 인가 서버가 이 MCP를 보호하는지만 알려 줍니다. 로그인 대상의 신원을 대신하지 않습니다. 클라이언트는 그 서버의 메타데이터를 다시 가져와 issuer를 맞춥니다. 리소스 메타데이터가 A를 가리키고 인가 서버 문서가 자신을 B라고 하면 멈춥니다. 연동을 먼저 통과시키려고 B를 pin하지 마십시오.
2026-07-28 개정은 동적 등록을 낮추고 Client ID Metadata Document를 권합니다. 안정적인 HTTPS에 클라이언트 JSON을 올리고 그 URL이 client_id입니다. SDK는 client_metadata_url로 받습니다. 인가 서버가 지원을 알리면 /register로 새 id와 secret을 받지 않습니다. URL은 루트가 아닌 경로가 있는 HTTPS여야 하고 생성 때 검사합니다. 저장된 client_info가 이 문서보다 우선이므로 옛 기록을 두면 새로 고정한 issuer가 적용되지 않습니다.
옛 client_info를 지우고, 비밀 교체는 따로 한다
수정 버전은 저장한 등록에 로그인 대상을 표시합니다. 업그레이드 전에 쓴 client_info에는 그 표시가 없습니다. 안내는 이 기록을 지워 다음 등록이 새 규칙으로 다시 만들어지고 올바른 로그인 대상에 묶이게 하라고 합니다. 패키지만 올리고 옛 파일을 두면 프로세스는 묶이지 않은 등록을 계속 꺼냅니다.
기록을 지우는 일과 비밀을 교체하는 일은 다릅니다. client_info를 지우는 것은 새 코드가 다시 등록하게 하려는 것입니다. client secret 교체와 토큰 취소는 신뢰하지 않는 서버에 자격 증명을 넘겼을 수 있기 때문입니다. 그 이력이 없으면 내부 클라이언트 비밀을 전부 바꿀 필요는 없습니다. 이력이 있으면 로그인 대상에서 하고, 채팅에 secret을 붙이지 마십시오.
콜백의 state와 iss는 도착한 그대로 Provider에 돌려줍니다. Provider는 state를 자신이 만든 값과, iss를 발견한 issuer와 맞춥니다. 둘은 섞임을 막는 필드이고 디버그 항목이 아닙니다. 콜백을 직접 파싱하면서 iss를 버리면 수정 버전이 넣은 대조를 떼는 것입니다.
고정한 issuer를 저장소에 두고 계약으로 검토한다
연결할 MCP 엔드포인트마다 레코드 하나입니다. 서버 URL과 허용하는 issuer. 값은 인가 서버 메타데이터 원문에서 옮깁니다. 리뷰하는 것은 포맷한 JSON이고 누군가의 메모에 적힌 도메인이 아닙니다. 릴리스 사이에 diff를 봐 누가 issuer를 다른 호스트로 옮겼는지 확인합니다.
저장소에 남기는 것은 이 대조입니다. client_secret은 남기지 않습니다. 비밀은 비밀 관리자에 있고, 여기서 고정하는 것은 로그인 대상뿐입니다.
{
"clients": [
{
"mcp_server": "https://orders.example.com/mcp",
"issuer": "https://auth.example.com"
}
]
}
머신 간 생성자와 이 목록은 같은 문자열을 씁니다. 목록이 https://auth.example.com인데 코드에 슬래시가 하나 더 있으면 수정 버전은 다른 대상으로 보고 멈춥니다. 원하는 실패가 이것입니다. 작업은 OAuthFlowError로 붉어지고, token 엔드포인트를 조용히 바꾸지 않습니다.
커밋 전에 JSON 포매터로 메타데이터와 issuer 목록을 펼치고, JSON Schema 검증으로 issuer와 token_endpoint가 문자열인지 확인한 다음, JSON 비교로 누가 로그인 대상을 바꿨는지 봅니다.
자주 묻는 질문
로컬 stdio 서버만 쓰면 이 안내를 봐야 하나요?
stdio 클라이언트는 영향 밖입니다. 같은 환경에 이 패키지의 HTTP 클라이언트도 있으면 그 버전으로 올립니다. 잠금 파일에 1.9.1–1.29.1 또는 2.0.0–2.1.1이 있으면 해당 수정 버전으로 올립니다. 「주로 stdio」라고 건너뛰지 않습니다.
1.30.0으로 올렸는데 로그에 경고가 없으면 끝난 건가요?
그렇게 판단할 수 없습니다. ClientCredentialsOAuthProvider와 PrivateKeyJWTOAuthProvider에 issuer가 없을 때의 알림은 기본적으로 숨는 DeprecationWarning입니다. deprecation 경고를 켜거나 생성자에 issuer를 적으십시오. 경고가 안 보인다고 인자가 들어간 것은 아닙니다.
동적 등록과 Client ID Metadata Document 중 무엇을 쓸까요?
인가 서버가 client_id_metadata_document_supported를 알리면 HTTPS의 클라이언트 JSON을 쓰고 그 URL을 client_id로 둡니다. /register로 매번 secret을 받지 않습니다. 아직 지원하지 않으면 SDK는 동적 등록으로 돌아갑니다. 어느 쪽이든 저장된 client_info가 우선이므로 올린 뒤 옛 기록을 지웁니다.
서버 쪽 OAuth 2.1 글과 어떻게 맞추나요?
서버 글은 리소스 서버가 access token을 어떻게 요구하는지를 다룹니다. 이 글은 클라이언트가 로그인 대상을 잘못 따라가지 않게 하는 일입니다. 원격 서버가 자기 게이트웨이 밖에 있으면 둘 다 읽습니다. 서버는 토큰을 검사하고, 클라이언트는 issuer를 생성자와 저장소 목록에 고정합니다.
정리
9월 28일 안내는 「발견 결과가 말하는 로그인 대상을 그대로 믿는다」를 기본에서 뺐습니다. HTTP 클라이언트는 1.30.0 또는 2.2.0으로 올립니다. 머신 간에는 메타데이터 원문과 같은 issuer를 넘깁니다. 폐기된 RFC7523 Provider는 바꿉니다.
그다음 옛 client_info를 지웁니다. 신뢰하지 않는 서버에 연결한 적이 있으면 로그인 대상에서 secret을 교체하고 토큰을 취소합니다. 저장소에는 서버 URL과 issuer 대조만 두고, 포맷, 검증, 비교로 그 문자열을 지킵니다.