docs: refresh translations, and translate pages in parallel (#3458)
This commit is contained in:
parent
08a3bc8eaf
commit
fd66270926
5 changed files with 4821 additions and 2277 deletions
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3]
|
||||
sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9]
|
||||
tool: 1
|
||||
---
|
||||
# 기존 앱에 추가하기 {#add-to-an-existing-app}
|
||||
|
|
@ -98,7 +98,7 @@ Starlette의 `Host` 라우트도 같은 방식으로 동작합니다. 경로 대
|
|||
--8<-- "docs_src/asgi/tutorial004.py"
|
||||
```
|
||||
|
||||
이제 클라이언트는 `/notes/mcp`가 아니라 `/notes`에 연결합니다.
|
||||
이제 클라이언트는 `/notes/mcp`가 아니라 `/notes/`에 연결합니다.
|
||||
|
||||
## 브라우저 클라이언트를 위한 CORS {#cors-for-browser-clients}
|
||||
|
||||
|
|
@ -141,4 +141,4 @@ Starlette의 `Host` 라우트도 같은 방식으로 동작합니다. 경로 대
|
|||
* 브라우저 클라이언트에는 CORS가 필요합니다. `Mcp-*` 요청 헤더를 위한 `allow_headers`, 응답을 위한 `expose_headers=["Mcp-Session-Id"]`입니다.
|
||||
* `@mcp.custom_route()`는 `/mcp` 옆에 인증되지 않는 평범한 HTTP 엔드포인트를 추가합니다.
|
||||
|
||||
서버가 실제 URL로 도달 가능해지면, **[클라이언트](../client/index.md)**는 서버 객체 대신 그 URL로 연결합니다.
|
||||
서버가 실제 URL로 도달 가능해지면, **[클라이언트](../client/index.md)**는 그 URL로 서버에 연결합니다.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31]
|
||||
sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6]
|
||||
tool: 1
|
||||
---
|
||||
# 인가 {#authorization}
|
||||
|
|
@ -23,12 +23,12 @@ OAuth 용어로 말하면 서버는 **리소스 서버**입니다. 누구도 로
|
|||
|
||||
SDK는 유효한 토큰이 어떤 모습인지에 대해 아무런 의견이 없습니다. **`TokenVerifier`**를 구현해서 알려 주면 됩니다.
|
||||
|
||||
```python title="server.py" hl_lines="12-14 19-24"
|
||||
```python title="server.py" hl_lines="14-16 21-27"
|
||||
--8<-- "docs_src/authorization/tutorial001.py"
|
||||
```
|
||||
|
||||
* `TokenVerifier`는 async 메서드 하나를 가진 프로토콜입니다. `verify_token`은 `Authorization` 헤더에서 꺼낸 원시 토큰을 받아, 유효하면 **`AccessToken`**을, 유효하지 않으면 `None`을 반환합니다. 그 외에 구현할 것은 없습니다.
|
||||
* 이 예제는 테이블에서 토큰을 조회합니다. 실제 구현은 JWT 서명을 검증하거나 인가 서버의 토큰 인트로스펙션 엔드포인트를 호출합니다. 그 코드는 직접 작성하는 것이고, SDK는 호출만 합니다.
|
||||
* 이 예제는 테이블에서 토큰을 조회하며, 각 항목에는 그 토큰이 발급된 대상 리소스가 기록되어 있습니다. 실제 구현은 JWT 서명을 검증하거나 인가 서버의 토큰 인트로스펙션 엔드포인트를 호출하고, 토큰이 누구를 위해 발급되었는지(토큰의 `aud`)를 `AccessToken.resource`에 담아 알려 줍니다. 그 코드는 직접 작성하는 것이고, SDK는 호출만 합니다.
|
||||
* `token_verifier=`와 `auth=`는 항상 함께 다닙니다. 한쪽만 전달하면 `MCPServer(...)`가 요청을 하나도 처리하기 전에 `ValueError`를 발생시킵니다.
|
||||
|
||||
`AuthSettings`는 리소스 서버의 공개 정보입니다.
|
||||
|
|
@ -36,6 +36,10 @@ SDK는 유효한 토큰이 어떤 모습인지에 대해 아무런 의견이 없
|
|||
* `issuer_url`: 토큰을 발급하는 인가 서버입니다.
|
||||
* `resource_server_url`: 이 MCP 엔드포인트의 공개 URL입니다. 토큰이 **어떤** 리소스를 위한 것인지 지칭하며, 디스커버리 문서가 위치하는 곳이기도 합니다.
|
||||
* `required_scopes`: 모든 토큰이 이 스코프를 전부 가지고 있어야 합니다.
|
||||
* `validate_token_resource`: `AccessToken.resource`가 `resource_server_url`이 아닌 토큰은 모두 거부합니다. `resource_server_url`은 설정하고 이 값을 설정하지 않은 채로 두면 경고(`MCPDeprecationWarning`)가 발생하며 `False`로 동작합니다. 3.0에서는 리소스 서버의 기본값이 `True`가 됩니다.
|
||||
* 인가 서버가 클라이언트가 요청한 `resource`에 토큰을 묶어 발급한다면 이 옵션을 켜세요. MCP 클라이언트는 이 값을 항상 보냅니다. `resource_server_url`은 클라이언트가 실제로 연결하는 URL과 정확히 같게 유지하세요.
|
||||
* 인가 서버가 자체 audience 식별자(Auth0 API 식별자, Entra 애플리케이션 ID)를 사용한다면 이 옵션을 끄고, 대신 검증기에서 `aud`를 확인해 이 서버를 위한 토큰이 아니면 `None`을 반환하세요.
|
||||
* `aud` 값이 리스트라면 `resource_server_url`과 같은 항목을 `resource`에 넣으세요.
|
||||
|
||||
!!! tip
|
||||
SDK 저장소의 `examples/servers/simple-auth/`에는 실제 인가 서버의
|
||||
|
|
@ -91,7 +95,7 @@ SDK는 유효한 토큰이 어떤 모습인지에 대해 아무런 의견이 없
|
|||
|
||||
어떤 핸들러 안에서든 **`get_access_token()`**은 현재 요청에 대해 검증기가 반환한 `AccessToken`입니다.
|
||||
|
||||
```python title="server.py" hl_lines="4 32-35"
|
||||
```python title="server.py" hl_lines="4 35-38"
|
||||
--8<-- "docs_src/authorization/tutorial002.py"
|
||||
```
|
||||
|
||||
|
|
@ -125,6 +129,6 @@ SDK는 리소스 서버 쪽 절반을 제공합니다. 검증하고, 알리고,
|
|||
* `token_verifier=`와 `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])`는 항상 함께 다닙니다.
|
||||
* SDK는 `/.well-known/oauth-protected-resource/...`에 [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata를 게시하고, 인증되지 않은 요청에는 이 문서를 가리키는 `WWW-Authenticate` 헤더가 담긴 401로 응답합니다. 디스커버리는 이것이 전부입니다.
|
||||
* 어떤 핸들러에서든 `get_access_token()`이 곧 호출자입니다.
|
||||
* 인가는 HTTP의 관심사입니다. `stdio`와 인메모리 클라이언트에서는 인가가 전혀 보이지 않습니다.
|
||||
* 인가는 HTTP의 관심사입니다. `stdio`와 인메모리 테스트 클라이언트에서는 인가가 전혀 보이지 않습니다.
|
||||
|
||||
클라이언트 쪽 절반(인가 서버를 찾아내고 토큰을 대신 가져오는 일)은 **[OAuth 클라이언트](../client/oauth-clients.md)**에서 확인하세요. 그리고 사용자에게 신원을 묻는 대신 신원을 **어설션**하는 클라이언트는 **[ID 어설션](../client/identity-assertion.md)**에서 확인하세요.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a]
|
||||
sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d]
|
||||
tool: 1
|
||||
---
|
||||
# 배포와 확장 {#deploy-scale}
|
||||
|
|
@ -46,6 +46,22 @@ translation:
|
|||
경고 한 줄로만 나타납니다. 새로 배포한 서버가 모든 연결을 거부한다면, 달리 밝혀지기 전까지는
|
||||
Host 허용 목록 문제입니다. **[문제 해결](../troubleshooting.md)**도 여기서 시작합니다.
|
||||
|
||||
## TLS 종료 프록시 뒤에서 {#behind-a-tls-terminating-proxy}
|
||||
|
||||
TLS가 프록시(인그레스, 로드 밸런서, Caddy, nginx)에서 끝나고 그 뒤에서 uvicorn이 평문 HTTP를 서비스한다면, uvicorn이 프록시의 `X-Forwarded-*` 헤더를 신뢰하도록 설정하세요.
|
||||
|
||||
```console
|
||||
uvicorn server:app --proxy-headers --forwarded-allow-ips='<proxy address>'
|
||||
```
|
||||
|
||||
이 설정이 없으면 앱은 자신이 `http://`로 서비스되고 있다고 믿고, 앱이 내보내는 모든 리디렉션(흔한 것은 `/mcp` → `/mcp/`)이 `http://…`를 가리킵니다. Python 클라이언트는 HTTPS 엔드포인트에서 평문 HTTP로 따라가기를 거부하며 그 이유를 알려 줍니다.
|
||||
|
||||
```text
|
||||
MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP.
|
||||
```
|
||||
|
||||
클라이언트 쪽 임시방편은 서버가 서비스하는 정확한 URL(`https://mcp.example.com/mcp/`, 슬래시 포함)을 설정해 리디렉션이 일어나지 않게 하는 것입니다. 제대로 된 해결책은 위의 플래그입니다. `FORWARDED_ALLOW_IPS`는 같은 설정의 환경 변수 이름이며, `*` 값은 모든 홉을 신뢰하므로 프록시 말고는 아무것도 uvicorn에 닿을 수 없을 때만 맞는 설정입니다.
|
||||
|
||||
## 워커, 그리고 스티키가 필요한 쪽 {#workers-and-who-has-to-be-sticky}
|
||||
|
||||
호스트명이 응답하기 시작했다면 그 뒤에 워커를 둘 이상 두세요. 이를 위한 SDK 설정은 없습니다. Starlette 앱은 다른 ASGI 앱과 똑같이, 포크할 줄 아는 도구에 객체를 넘겨서 확장합니다.
|
||||
|
|
@ -170,6 +186,7 @@ python -c "import secrets; print(secrets.token_hex(32))"
|
|||
## 요약 {#recap}
|
||||
|
||||
* 기본적으로 이 앱은 localhost로 오는 요청만 받습니다. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])`가 서비스 개시의 관문입니다. 이를 전달하기 전까지 실제 호스트명 뒤의 모든 요청은 `421`이 되며, 그 이유는 서버 로그에만 남습니다.
|
||||
* TLS 종료 프록시 뒤에서는 uvicorn을 `--proxy-headers --forwarded-allow-ips=...` 옵션으로 실행하세요. 그러지 않으면 리디렉션이 `http://`를 가리키고 클라이언트가 이를 거부합니다.
|
||||
* 2026-07-28에는 세션이 없고, 로드 밸런서가 스티키로 붙들 대상도 없습니다. `stateless_http=True`는 레거시 전용 설정입니다. 최신 요청은 이 플래그를 읽기도 전에 라우팅되고 응답되기 때문입니다.
|
||||
* 기본 `requestState` 키는 프로세스마다 만들어지는 `os.urandom(32)`입니다. 다른 워커에 도달한 다중 왕복 재시도는 `-32602` *"Invalid or expired requestState"*로 실패합니다.
|
||||
* 해결책은 `RequestStateSecurity(keys=[...])`를 쓰는 것, **그리고** 모든 인스턴스에 같은 서버 이름을 쓰는 것입니다. 이름은 토큰의 기본 audience 클레임입니다. 같은 키, 같은 이름.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
|
||||
sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0]
|
||||
tool: 1
|
||||
---
|
||||
# 서버 실행하기 {#running-your-server}
|
||||
|
|
@ -75,6 +75,11 @@ Inspector는 실제 호스트가 하는 일을 그대로 합니다. `server.py`
|
|||
* `max_request_body_size`: 허용되는 요청 본문의 최대 크기(바이트). 기본값은 4MiB이며, 더 큰 요청은
|
||||
파싱이나 세션 생성 전에 HTTP 413을 받습니다. 정상적인 MCP 메시지가 이 크기를 넘을 때만
|
||||
올리세요.
|
||||
* `session_idle_timeout`: 진행 중인 것이 아무것도 없는 상태로 레거시 세션이 머물 수 있는 시간(초)이며, 이 시간이 지나면
|
||||
서버가 세션을 닫습니다. 기본값은 1800입니다. `None`이면 비활성화됩니다.
|
||||
[세션 수명과 제한](legacy-clients.md#session-lifetime-and-limits)을 참고하세요.
|
||||
* `max_sessions`: 한 프로세스가 동시에 유지하는 레거시 세션의 수. 기본값은 10 000입니다. `None`이면
|
||||
제한이 없어집니다. 같은 섹션에서 다룹니다.
|
||||
* `event_store`, `retry_interval`, `transport_security`: 재개 가능성과 DNS 리바인딩 보호. localhost가 아닌 곳에 배포하기 전까지는 미뤄도 됩니다. `transport_security`는 **[배포와 확장](deploy.md)**에서 다룹니다.
|
||||
|
||||
!!! warning
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c]
|
||||
sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c]
|
||||
tool: 1
|
||||
---
|
||||
# 레거시 클라이언트 지원 {#serving-legacy-clients}
|
||||
|
|
@ -21,15 +21,25 @@ SDK는 모든 요청을 `MCP-Protocol-Version` 헤더에 따라 라우팅합니
|
|||
|
||||
## 하나의 핸들러, 두 시대 {#one-handler-both-eras}
|
||||
|
||||
다음은 사용자에게 무언가를 물어봐야 하는 도구와, 그 도구를 호출하는 두 시대의 클라이언트입니다.
|
||||
다음은 사용자에게 무언가를 물어봐야 하는 도구입니다.
|
||||
|
||||
```python title="server.py" hl_lines="24 37-38"
|
||||
```python title="server.py" hl_lines="21"
|
||||
--8<-- "docs_src/legacy_clients/tutorial001.py"
|
||||
```
|
||||
|
||||
`reserve`에는 모델이 제공하지 않은 정보가 하나 필요합니다. 몇 권인지입니다. `Annotated[..., Resolve(ask_quantity)]`는 도구가 이를 선언하는 방법입니다(자세한 내용은 **[의존성](../handlers/dependencies.md)**에서 확인하세요). `reserve` 안에는 버전을 명시하거나, 기능을 확인하거나, 분기하는 코드가 전혀 없습니다.
|
||||
|
||||
두 클라이언트는 같은 `mcp` 객체에 **동시에** 열려 있습니다. `mode="legacy"`는 `initialize` 핸드셰이크를 실행합니다. 2026 이전 클라이언트가 여는 바로 그 연결입니다. 다른 하나는 기본값을 사용해 `2026-07-28` 버전으로 연결됩니다.
|
||||
HTTP로 띄우세요. 다음은 이 도구를 호출하는 두 시대의 클라이언트입니다.
|
||||
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
```python title="client.py" hl_lines="14-15"
|
||||
--8<-- "docs_src/legacy_clients/tutorial001_client.py"
|
||||
```
|
||||
|
||||
두 클라이언트는 실행 중인 같은 서버를 상대로 **동시에** 열려 있습니다. `mode="legacy"`는 `initialize` 핸드셰이크를 실행합니다. 2026 이전 클라이언트가 여는 바로 그 연결입니다. 다른 하나는 기본값을 사용해 `2026-07-28` 버전으로 연결됩니다. 두 번째 터미널에서 `python client.py`를 실행하세요.
|
||||
|
||||
```text
|
||||
2025-11-25 {'result': "Reserved 2 of 'Dune'."}
|
||||
|
|
@ -61,6 +71,31 @@ SDK는 모든 요청을 `MCP-Protocol-Version` 헤더에 따라 라우팅합니
|
|||
세션에 다시 연결하는 클라이언트에게 놓친 SSE 이벤트를 재생하는 것)입니다. 다른 프로세스에서
|
||||
세션에 도달할 수 있게 해 주는 일은 결코 없습니다.
|
||||
|
||||
## 세션 수명과 제한 {#session-lifetime-and-limits}
|
||||
|
||||
레거시 세션은 영원히 살지 않으며, 한 프로세스가 무한히 많은 세션을 보유하지도 않습니다. 이를 제어하는 설정이 두 가지 있습니다. 둘 다 `run()`, `streamable_http_app()`, `Server.streamable_http_app()`의 키워드 인자입니다. 현대(`2026-07-28`) 연결과 `stateless_http=True`에는 세션이 없으므로 두 설정 모두 적용되지 않습니다.
|
||||
|
||||
| 설정 | 기본값 | 하는 일 | 클라이언트가 보는 것 | 끄는 방법 |
|
||||
|---|---|---|---|---|
|
||||
| `session_idle_timeout` | `1800`(30분) | 그 시간 동안 진행 중인 것이 아무것도 없었던 세션을 닫습니다. | `404 Session not found`. 다시 `initialize`해야 합니다. | `None` |
|
||||
| `max_sessions` | `10_000` | 그 수를 넘는 세션은 열기를 거부합니다. 기존 세션은 건드리지 않으며 아무것도 쫓아내지 않습니다. | JSON-RPC 코드 `-32603`과 함께 `503 Too many open sessions`. | `None` |
|
||||
|
||||
"진행 중"으로 간주되는 것은 다음과 같습니다.
|
||||
|
||||
* 열린 `GET` 스트림. SDK 클라이언트는 하나를 열어 두므로, 연결된 클라이언트의 세션은 만료되지 않습니다.
|
||||
* 아직 응답 중인 요청. 타임아웃보다 오래 실행되는 도구 호출은 중단되지 않으며, 카운트다운은 호출이 끝난 뒤에야 시작됩니다.
|
||||
* 그 밖에는 없습니다. 요청 사이에는 시계가 돌아갑니다. 세션에 들어오는 요청은 무엇이든 시계를 다시 시작하며, `ping`도 포함됩니다. 세션이 한 번 만료되면 무엇으로도 되살릴 수 없습니다.
|
||||
|
||||
`DELETE`로 세션을 끝내는 클라이언트는 세션을 즉시 해제합니다. 여는 요청이 거부된 클라이언트도 마찬가지입니다.
|
||||
|
||||
```python
|
||||
mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000)
|
||||
```
|
||||
|
||||
두 이벤트 모두 서버 로그에 나타납니다. 만료는 `INFO` 수준의 `Session <id> idle timeout`입니다. 열기 거부는 `WARNING` 수준의 `Refusing to open a new session: <n> sessions are already open`입니다.
|
||||
|
||||
제한은 프로세스별입니다. 워커가 넷이면 상한은 `max_sessions`의 네 배이며, 각 워커는 자기 세션을 스스로 만료시킵니다.
|
||||
|
||||
## 유일한 옵션: `stateless_http` {#the-one-knob-stateless_http}
|
||||
|
||||
스티키 라우팅이 치르기 싫은 비용이라면, 바꿀 수 있는 것은 정확히 하나입니다.
|
||||
|
|
@ -85,7 +120,7 @@ SDK는 모든 요청을 `MCP-Protocol-Version` 헤더에 따라 라우팅합니
|
|||
|
||||
!!! check
|
||||
일부러 잘못된 설정을 해 보세요. `reserve`는 방금 두 클라이언트를 모두 지원한 바로 그 도구입니다.
|
||||
`stateless_http=True`로 배포하고, 같은 두 클라이언트를 HTTP로 연결한 뒤, 각각에서 호출해 보세요.
|
||||
`stateless_http=True`로 배포하고, 같은 두 클라이언트를 연결한 뒤, 각각에서 호출해 보세요.
|
||||
|
||||
현대 클라이언트는 여전히 `Reserved 2 of 'Dune'.`을 받습니다. 현대 경로는 바뀌지 않았습니다.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue