docs: refresh translations, and translate pages in parallel (#3458)
This commit is contained in:
parent
08a3bc8eaf
commit
fd66270926
6 changed files with 4821 additions and 2277 deletions
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
|
||||
sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800]
|
||||
tool: 1
|
||||
---
|
||||
# 캐싱 힌트 {#caching-hints}
|
||||
|
|
@ -30,7 +30,7 @@ translation:
|
|||
|
||||
저수준 `Server`에서는 핸들러가 결과를 직접 조립하며, `ttl_ms` / `cache_scope`는 결과 모델의 필드일 뿐입니다. 이 필드를 명시적으로 설정한 핸들러는 필드 단위로 언제나 생성자 맵보다 우선합니다.
|
||||
|
||||
```python title="server.py" hl_lines="10 16"
|
||||
```python title="server.py" hl_lines="11 17"
|
||||
--8<-- "docs_src/caching/tutorial002.py"
|
||||
```
|
||||
|
||||
|
|
@ -44,10 +44,24 @@ translation:
|
|||
|
||||
2026-07-28 세션에서는 `Client`가 힌트를 대신 따릅니다. 기본으로 켜져 있는 내장 응답 캐시가 있기 때문입니다. `ttlMs`를 싣고 도착한 결과는 저장되고, 그 TTL 안에 동일한 호출이 오면 왕복 없이 캐시에서 제공됩니다. 힌트가 **없는** 결과는 캐시되지 않습니다. 힌트 없는 결과에는 `CacheConfig.default_ttl_ms`가 적용되는데 기본값이 `0`(즉시 만료)이므로, 아무것도 선언하지 않는 서버는 늘 그랬듯 호출마다 요청이 오는 트래픽을 그대로 보게 됩니다.
|
||||
|
||||
```python title="client.py" hl_lines="33 35 38"
|
||||
이 동작을 직접 확인하려면 앞 절의 `server.py`를 uvicorn으로 띄우세요(마지막 줄이 ASGI 앱을 만듭니다). 핸들러는 실제로 실행될 때마다 한 줄을 출력합니다.
|
||||
|
||||
```console
|
||||
uvicorn server:app --port 8000
|
||||
```
|
||||
|
||||
```python title="client.py" hl_lines="20 23 28"
|
||||
--8<-- "docs_src/caching/tutorial003.py"
|
||||
```
|
||||
|
||||
두 번째 터미널에서 `python client.py`를 실행하세요. 첫 번째 결과가 싣고 온 힌트, 즉 핸들러의 `ttlMs`와 맵의 `cacheScope`를 나란히 출력합니다.
|
||||
|
||||
```text
|
||||
1000 public
|
||||
```
|
||||
|
||||
나머지는 서버 쪽 터미널이 알려 줍니다. uvicorn의 요청 로그 사이로 `tools/list served`가 세 번 나타납니다.
|
||||
|
||||
호출은 네 번, 서버에서 가져온 것은 세 번입니다. 두 번째 호출은 신선한 항목을 찾았고 서버에 도달하지 않았습니다. (주입된) 시계를 TTL 너머로 진행시키자 세 번째 호출은 다시 가져왔고, 네 번째 호출은 `cache_mode="refresh"`를 지정했습니다. 이 키워드 인자는 캐싱 동사 다섯 개(`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`)에 있습니다.
|
||||
|
||||
* `"use"`(기본값)는 신선한 항목이 있으면 해당 항목을 제공하고, 없으면 가져와서 저장합니다.
|
||||
|
|
@ -56,7 +70,7 @@ translation:
|
|||
|
||||
`"use"` 위에 규칙이 하나 더 있습니다. **`meta`를 담은 호출은 언제나 서버에 도달합니다.** `meta`가 설정된 요청(진행률 토큰, 추적 필드)은 실제로 전송되는 요청을 기대하므로, `cache_mode="use"`에서는 `"refresh"`로 취급됩니다. 캐시 읽기는 건너뛰고, 가져온 결과는 여전히 캐시된 항목을 대체합니다. `"bypass"`와 명시적 `"refresh"`는 평소대로 동작합니다.
|
||||
|
||||
캐싱을 완전히 끄려면 `Client(server, cache=None)`으로 생성하세요. 모든 호출이 다시 왕복이 되며, `cache_mode`는 여전히 받아들여지지만 아무 일도 하지 않습니다.
|
||||
캐싱을 완전히 끄려면 `Client`를 생성할 때 `cache=None`을 전달하세요. 모든 호출이 다시 왕복이 되며, `cache_mode`는 여전히 받아들여지지만 아무 일도 하지 않습니다.
|
||||
|
||||
범위도 자동으로 존중됩니다. `"private"` 항목은 캐시의 **파티션**(아래 참고)을 키로 하고, `"public"` 항목은 더 넓은 공유를 선택할 수 있습니다. 그리고 알림이 지목하는 바로 그 항목에 대해서는 **알림이 TTL보다 우선합니다**. `list_changed` 알림은 일치하는 캐시된 목록을 축출하고, `resources/updated`는 정확히 그 URI로 저장된 캐시된 읽기 결과를 축출합니다. 얼마나 신선했든 상관없습니다. 2026-07-28 연결에서 이 알림은 `client.listen(...)`으로 여는 `subscriptions/listen` 스트림으로 도착하며, 축출은 감시자가 이벤트를 보기 전에 완료됩니다. 자세한 내용은 **[구독](subscriptions.md)**에서 확인하세요.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
|
||||
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3]
|
||||
tool: 1
|
||||
---
|
||||
# 클라이언트 콜백 {#client-callbacks}
|
||||
|
|
@ -63,7 +63,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')
|
|||
`Client(...)` 호출의 `mode="legacy"`는 실제로 중요한 역할을 합니다. 기본적으로 `Client(...)`는 최신
|
||||
프로토콜 경로를 협상하는데, 그 경로에는 서버에서 클라이언트로 가는 요청을 위한 역방향 채널이 없어서
|
||||
콜백이 실행되기도 전에 `ctx.elicit` 호출이 실패합니다. 이를 결정하는 것은 트랜스포트가 아니라 협상된
|
||||
프로토콜이며, 인메모리든 URL을 통하든 마찬가지입니다. 클라이언트가 이런 요청에 응답해야 할 때마다
|
||||
프로토콜입니다. 클라이언트가 이런 요청에 응답해야 할 때마다
|
||||
`mode="legacy"`로 고정하세요. 이 페이지를 뒷받침하는 모든 테스트가 그렇게 합니다. 자세한 내용은 **[프로토콜 버전](../protocol-versions.md)**에서 확인하세요.
|
||||
|
||||
2026-07-28 세션에서도 콜백이 쓸모없어지는 것은 아니며, 입력을 받는 방식이 다를 뿐입니다. 도구가
|
||||
|
|
@ -151,4 +151,4 @@ result.structured_content # {'result': ['elicitation']}
|
|||
* `sampling_callback`과 `list_roots_callback`도 같은 방식으로 동작하지만 지원 중단 예정 기능을 처리합니다. 최신 서버는 대신 다중 왕복 요청을 사용합니다.
|
||||
* `logging_callback`과 `message_handler`는 알림을 받습니다. 아무것도 선언하지 않습니다.
|
||||
|
||||
`Client(...)`의 첫 번째 인자는 트랜스포트 객체입니다. 모든 종류는 **[클라이언트 트랜스포트](transports.md)**에서 다룹니다.
|
||||
`Client(...)`의 첫 번째 인자가 트랜스포트를 결정합니다. 모든 종류는 **[클라이언트 트랜스포트](transports.md)**에서 다룹니다.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
|
||||
sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7]
|
||||
tool: 1
|
||||
---
|
||||
# 클라이언트 {#the-client}
|
||||
|
|
@ -11,13 +11,23 @@ translation:
|
|||
|
||||
## 첫 번째 클라이언트 {#your-first-client}
|
||||
|
||||
```python title="client.py" hl_lines="14-18"
|
||||
클라이언트에게는 통신할 서버가 필요합니다. 이 페이지의 모든 예제가 연결하는 대상이 바로 이 Bookshop 서버입니다. `server.py`로 저장하고 HTTP로 실행해 두세요.
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/client/tutorial001.py"
|
||||
```
|
||||
|
||||
맨 위의 서버는 연결할 대상을 마련하기 위해 있을 뿐입니다. 클라이언트는 강조 표시된 다섯 줄입니다.
|
||||
```console
|
||||
uv run mcp run server.py --transport streamable-http
|
||||
```
|
||||
|
||||
* `Client(mcp)`에는 **서버 객체 자체**를 넘깁니다. 이것이 인메모리 트랜스포트입니다. 서브프로세스도, 포트도, HTTP도 없습니다. 이 페이지의 모든 예제와 앞으로 작성할 모든 테스트가 이 방식으로 연결합니다.
|
||||
이렇게 하면 `http://localhost:8000/mcp`에서 서버가 제공됩니다. 클라이언트는 별도의 프로그램입니다. `client.py`로 저장하고 두 번째 터미널에서 `python client.py`를 실행하세요.
|
||||
|
||||
```python title="client.py" hl_lines="7-11"
|
||||
--8<-- "docs_src/client/tutorial001_client.py"
|
||||
```
|
||||
|
||||
* `Client("http://localhost:8000/mcp")`에는 **URL**을 넘기므로, 방금 시작한 서버에 Streamable HTTP로 연결합니다.
|
||||
* `async with`가 **생명 주기**입니다. 들어가면 연결하고 협상하며, 나오면 연결을 끊습니다. `connect()` / `close()` 쌍은 없으며, 블록이 끝난 뒤에는 `Client`를 재사용할 수 없습니다.
|
||||
* 블록 안에서는 연결 정보가 이미 평범한 프로퍼티로 준비되어 있습니다.
|
||||
|
||||
|
|
@ -25,10 +35,10 @@ translation:
|
|||
|
||||
`Client`는 위치 인자 하나를 받고, 그 타입으로 트랜스포트를 결정합니다.
|
||||
|
||||
* `MCPServer`(또는 저수준 `Server`) 인스턴스: **프로세스 내부**에서 연결합니다.
|
||||
* URL 문자열(`Client("http://localhost:8000/mcp")`): 프로덕션 경로인 Streamable HTTP입니다.
|
||||
* `StdioServerParameters`: **서브프로세스**로 실행할 명령이며, 그 stdin과 stdout을 통해 통신합니다.
|
||||
* URL 문자열(`Client("http://localhost:8000/mcp")`): 실제 배포에 쓰는 트랜스포트인 Streamable HTTP입니다.
|
||||
* `StdioServerParameters`: 로컬 **서브프로세스**로 실행할 명령이며, 그 stdin과 stdout을 통해 통신합니다.
|
||||
* **트랜스포트**: `async with ... as (read, write)`로 사용할 수 있는 모든 것, 예를 들어 직접 만든 HTTP 클라이언트를 감싸는 `streamable_http_client(url, http_client=...)`입니다.
|
||||
* `MCPServer`(또는 저수준 `Server`) 인스턴스: 서브프로세스도 포트도 없이 **프로세스 내부**에서 연결합니다. 이 방식은 테스트를 위한 것이며, **[테스트](../get-started/testing.md)**가 이를 기반으로 합니다.
|
||||
|
||||
이 페이지의 나머지 내용은 네 가지 모두에서 동일합니다. 헤더, 서브프로세스, 타임아웃, `Transport` 프로토콜은 별도의 페이지인 **[클라이언트 트랜스포트](transports.md)**에서 다룹니다.
|
||||
|
||||
|
|
@ -49,11 +59,11 @@ translation:
|
|||
|
||||
## 도구 목록 조회 {#listing-tools}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
```python title="client.py" hl_lines="8-13"
|
||||
--8<-- "docs_src/client/tutorial002.py"
|
||||
```
|
||||
|
||||
`list_tools()`는 `ListToolsResult`를 반환하며, 도구는 `.tools`에 들어 있습니다. 각 도구는 호스트가 모델에 건네는 완전한 정의입니다.
|
||||
`list_tools()`는 `ListToolsResult`를 반환하며, 도구는 `.tools`에 들어 있습니다. 각 도구는 호스트가 모델에 건네는 완전한 정의입니다. 첫 번째 도구는 다음과 같습니다.
|
||||
|
||||
```python
|
||||
tool.name # 'search_books'
|
||||
|
|
@ -77,6 +87,8 @@ tool.description # 'Search the catalog by title or author.'
|
|||
|
||||
이 스키마는 UI가 인자 입력 폼을 렌더링하는 데 필요한 전부이자, 모델이 유효한 인자를 만들어 내는 데 필요한 전부입니다.
|
||||
|
||||
두 번째 도구인 `lookup_book`은 `title=` 없이 등록되었으므로 `tool.title`이 `None`입니다.
|
||||
|
||||
!!! tip
|
||||
`title`은 선택 사항이므로, 사람에게 도구를 보여 주는 UI는 무엇을 표시할지 골라야 합니다. `title`이 있으면 쓰고,
|
||||
없으면 `name`을 씁니다. `from mcp.shared.metadata_utils import get_display_name`이 정확히 그 일을 하며,
|
||||
|
|
@ -86,7 +98,7 @@ tool.description # 'Search the catalog by title or author.'
|
|||
|
||||
`call_tool(name, arguments)`는 도구를 실행하고 `CallToolResult`를 돌려줍니다.
|
||||
|
||||
```python title="client.py" hl_lines="27-34"
|
||||
```python title="client.py" hl_lines="9-16"
|
||||
--8<-- "docs_src/client/tutorial003.py"
|
||||
```
|
||||
|
||||
|
|
@ -142,7 +154,7 @@ result.is_error # False
|
|||
|
||||
리소스 동작은 짝을 이룹니다. 목록을 조회하는 방법이 둘, 읽는 방법이 하나입니다.
|
||||
|
||||
```python title="client.py" hl_lines="22-31"
|
||||
```python title="client.py" hl_lines="9-18"
|
||||
--8<-- "docs_src/client/tutorial004.py"
|
||||
```
|
||||
|
||||
|
|
@ -156,7 +168,7 @@ result.is_error # False
|
|||
|
||||
## 프롬프트 {#prompts}
|
||||
|
||||
```python title="client.py" hl_lines="15-20"
|
||||
```python title="client.py" hl_lines="8-13"
|
||||
--8<-- "docs_src/client/tutorial005.py"
|
||||
```
|
||||
|
||||
|
|
@ -181,7 +193,7 @@ message.content # TextContent(type='text', text='Recommend one poetry book from
|
|||
|
||||
자동 완성 핸들러가 있는 서버는 사용자가 입력하는 동안 프롬프트와 리소스 템플릿 인자를 자동 완성할 수 있습니다.
|
||||
|
||||
```python title="client.py" hl_lines="27-31"
|
||||
```python title="client.py" hl_lines="9-13"
|
||||
--8<-- "docs_src/client/tutorial006.py"
|
||||
```
|
||||
|
||||
|
|
@ -194,21 +206,21 @@ message.content # TextContent(type='text', text='Recommend one poetry book from
|
|||
|
||||
모든 `list_*` 메서드는 `cursor=` 키워드를 받고, 모든 결과에는 `next_cursor`가 있습니다. `next_cursor`가 `None`이면 전부 받은 것입니다.
|
||||
|
||||
```python title="client.py" hl_lines="22-30"
|
||||
```python title="client.py" hl_lines="7-15"
|
||||
--8<-- "docs_src/client/tutorial007.py"
|
||||
```
|
||||
|
||||
이 루프는 어떤 서버에 대해서도 올바릅니다. `MCPServer`는 모든 것을 한 페이지에 반환하므로 `next_cursor`는 `None`이고 루프는 한 번만 돌며, 그래서 대부분의 코드는 이 루프를 작성하지 않습니다. 실제로 페이지를 나누는 서버와 커서가 따르는 규칙은 **[페이지네이션](../advanced/pagination.md)**에서 다룹니다.
|
||||
`list_all_tools`는 어떤 서버에 대해서도 올바르게 동작합니다. `MCPServer`는 모든 것을 한 페이지에 반환하므로 `next_cursor`는 `None`이고 루프는 한 번만 돌며, 그래서 대부분의 코드는 이 루프를 작성하지 않습니다. 실제로 페이지를 나누는 서버와 커서가 따르는 규칙은 **[페이지네이션](../advanced/pagination.md)**에서 다룹니다.
|
||||
|
||||
## 테스트에서 {#in-tests}
|
||||
|
||||
프로세스도 포트도 없는 `Client(mcp)`는 그 자체로 이미 서버의 테스트 하네스입니다.
|
||||
이 페이지의 모든 `client.py`는 HTTP로 `server.py`에 연결했습니다. 테스트에서는 네트워크를 건너뛰고 `Client`에 서버 객체 자체를 넘깁니다. `from server import mcp`를 한 다음 `Client(mcp)`를 만들면 됩니다. 프로세스도 포트도 없으며, 위의 모든 메서드가 똑같이 동작합니다.
|
||||
|
||||
이를 위해 만들어진 생성자 플래그가 하나 있습니다. `Client(mcp, raise_exceptions=True)`입니다. 인메모리 연결에서만 효과가 있으며, 이를 설명하고 전체 패턴을 구축하는 페이지는 **[테스트](../get-started/testing.md)**입니다.
|
||||
이를 위해 만들어진 생성자 플래그가 하나 있습니다. `Client(mcp, raise_exceptions=True)`입니다. 프로세스 내부 연결에서만 효과가 있으며, 이를 설명하고 전체 패턴을 구축하는 페이지는 **[테스트](../get-started/testing.md)**입니다.
|
||||
|
||||
## 요약 {#recap}
|
||||
|
||||
* `Client(x)`는 서버 객체에는 인메모리로, URL 문자열에는 Streamable HTTP로, 그 밖의 것에는 트랜스포트를 통해 연결합니다.
|
||||
* `Client(x)`는 URL 문자열에는 Streamable HTTP로 연결하고, `StdioServerParameters`에는 서브프로세스를 실행하며, 트랜스포트에는 직접 들어가고, 테스트에서는 서버 객체 자체를 받습니다.
|
||||
* `async with`가 생명 주기의 전부입니다. 그 안에서는 `server_capabilities`와 `protocol_version`이 이미 채워져 있으며, 서버가 제공하는 경우 `server_info`와 `instructions`도 마찬가지입니다.
|
||||
* `list_tools()`는 각 도구의 `name`, `title`, `description`, `input_schema`를 제공합니다.
|
||||
* `call_tool()`은 모델을 위한 `content`, 코드를 위한 `structured_content`, 그리고 `is_error`를 반환합니다. 예외를 발생시키는 도구는 예외가 아니라 결과입니다.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b]
|
||||
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b]
|
||||
tool: 1
|
||||
---
|
||||
# OAuth 클라이언트 {#oauth-clients}
|
||||
|
|
@ -81,18 +81,20 @@ translation:
|
|||
|
||||
`Client`가 처음 요청을 보내면 서버는 `401`로 응답합니다. 그러면 프로바이더가 이어받습니다.
|
||||
|
||||
1. **디스커버리.** `WWW-Authenticate` 헤더를 읽고, `/.well-known/oauth-protected-resource`에서 서버의 Protected Resource Metadata를 가져오고, 어느 인가 서버가 이 리소스를 보호하는지 알아낸 뒤, **그** 서버의 메타데이터를 가져옵니다.
|
||||
1. **디스커버리.** `WWW-Authenticate` 헤더를 읽고, `/.well-known/oauth-protected-resource`에서 서버의 Protected Resource Metadata를 가져오고, 어느 인가 서버가 이 리소스를 보호하는지 알아낸 뒤, **그** 서버의 메타데이터를 가져옵니다. (리소스 메타데이터를 게시하지 않는 오래된 서버에는 대신 그 서버 자신의 오리진에서 인가 서버 메타데이터를 요청합니다.) 어느 경우든 메타데이터의 `issuer` 값은 그 메타데이터를 가져온 대상 서버를 가리켜야 하며, 다른 값이면 거부합니다.
|
||||
2. **등록.** 저장소에 아무것도 없으면 `OAuthClientMetadata`로 동적으로 등록하고 결과를 저장합니다.
|
||||
3. **인가.** PKCE 쌍과 `state`를 생성하고, 인가 URL을 조립하고, `redirect_handler`를 await한 다음, 코드를 받기 위해 `callback_handler`를 await합니다.
|
||||
4. **교환.** 코드를 `OAuthToken`으로 교환해 저장하고, 원래 요청에 `Authorization: Bearer ...`를 붙여 다시 보냅니다.
|
||||
|
||||
그다음부터는 조용합니다. 토큰은 저장소에서 꺼내 쓰고, 만료된 액세스 토큰은 리프레시 토큰으로 갱신하며, 그 어느 것도 통하지 않을 때에만 흐름을 다시 실행합니다.
|
||||
|
||||
이 모든 요청에 적용되는 트랜스포트 규칙이 하나 있습니다. 바깥의 MCP 요청과 마찬가지로, 같은 오리진에 머물면서 메서드를 유지하는 리다이렉트(이를테면 끝 슬래시를 붙이는 307/308)만 따르고, 그 밖의 리다이렉트는 해당 URL이 응답하지 않는 것으로 취급합니다.
|
||||
|
||||
이 가운데 직접 작성한 코드는 하나도 없습니다. 키워드 인자가 두 개 더 남아 있는데(`client_metadata_url`과 `validate_resource_url`), 이 파일에는 둘 다 필요 없습니다. 알아 둘 만한 것은 `client_metadata_url`이며, 아래에 별도 섹션이 있습니다.
|
||||
|
||||
### 직접 해 보기 {#try-it}
|
||||
|
||||
이 문서의 예제 대부분은 인메모리 `Client(server)`로 확인할 수 있습니다. 이 예제는 아닙니다. 이 흐름의 핵심이 HTTP `401`인데, 인메모리 클라이언트와 서버 사이에는 HTTP가 없기 때문입니다.
|
||||
테스트에서 쓰는 인메모리 `Client(server)`는 여기서는 도움이 되지 않습니다. 이 흐름의 핵심이 HTTP `401`인데, 인메모리 클라이언트와 서버 사이에는 HTTP가 없기 때문입니다.
|
||||
|
||||
리포지토리에는 실제로 동작하는 버전이 들어 있습니다. `examples/servers/simple-auth/`는 독립 실행형 인가 서버와 보호된 MCP 서버를 실행하고, `examples/clients/simple-auth-client/`는 이 페이지의 클라이언트를 작은 CLI로 키운 것입니다. 그 README에 두 명령이 있습니다. 서버를 시작하고, 그 서버를 대상으로 클라이언트를 실행하면 네 단계가 지나가는 모습을 볼 수 있습니다.
|
||||
|
||||
|
|
@ -110,13 +112,14 @@ URL은 루트가 아닌 경로를 가진 HTTPS여야 합니다. 그 외에는
|
|||
|
||||
`ClientCredentialsOAuthProvider`는 사람만 빠진 똑같은 `httpx2.Auth`입니다.
|
||||
|
||||
```python title="client.py" hl_lines="4 27-33"
|
||||
```python title="client.py" hl_lines="4 27-34"
|
||||
--8<-- "docs_src/oauth_clients/tutorial002.py"
|
||||
```
|
||||
|
||||
달라진 점은 다음과 같습니다.
|
||||
|
||||
* `OAuthClientMetadata`도 핸들러도 없습니다. `client_id`와 `client_secret`을 전달하면 프로바이더가 이를 감싸는 최소한의 `client_credentials` 등록을 만들고 동적 등록은 완전히 건너뜁니다.
|
||||
* `issuer`는 그 자격 증명을 발급한 인가 서버를 지목합니다. 그 서버의 `/.well-known/oauth-authorization-server` 문서가 반환하는 `issuer` 값을 쓰세요. 디스커버리는 위와 똑같이 실행되지만, 토큰 요청은 오직 **그** 발급자의 메타데이터로만 조립됩니다. MCP 서버가 다른 곳을 가리키면 흐름은 대신 `OAuthFlowError`와 함께 멈춥니다. 이 값을 생략하는 것은 지원 중단 예정이며 3.0에서는 필수가 됩니다(**[지원 중단 예정 기능](../deprecated.md#deprecated-sdk-helpers)** 참고). 그때까지는 프로바이더가 경고를 내고 디스커버리가 찾아낸 인가 서버를 그대로 씁니다.
|
||||
* `scope`는 공백으로 구분한 문자열로, OAuth의 전송 형식입니다.
|
||||
* 그 아래는 모두 동일합니다. 같은 `TokenStorage`, 같은 `httpx2.AsyncClient(auth=...)`, 같은 `streamable_http_client`를 씁니다.
|
||||
|
||||
|
|
@ -129,8 +132,8 @@ URL은 루트가 아닌 경로를 가진 HTTPS여야 합니다. 그 외에는
|
|||
`mcp.client.auth.extensions.client_credentials`에는 프로바이더가 하나 더 있습니다.
|
||||
공유 시크릿 대신 JWT로 인증하는 클라이언트를 위한 **`PrivateKeyJWTOAuthProvider`**입니다
|
||||
(`private_key_jwt`, 즉 키 쌍과 워크로드 아이덴티티 방식). 같은 패턴을 따릅니다.
|
||||
하나를 생성해 `auth=`에 넣으면 됩니다. 같은 모듈에는 그 어설션을 만드는 두 헬퍼인
|
||||
`SignedJWTParameters`와 `static_assertion_provider`도 들어 있습니다.
|
||||
하나를 생성해(선택 인자 `issuer`를 똑같이 받습니다) `auth=`에 넣으면 됩니다. 같은 모듈에는
|
||||
그 어설션을 만드는 두 헬퍼인 `SignedJWTParameters`와 `static_assertion_provider`도 들어 있습니다.
|
||||
|
||||
사람이 없는 상황이 하나 더 있습니다. 클라이언트가 기업에 속해 있고, 어느 MCP 서버에 접근할 수 있는지를 사용자가 아니라 그 기업의 아이덴티티 공급자가 결정하는 경우입니다. 이는 고유한 신뢰 모델을 가진 다른 그랜트이며, 별도 페이지인 **[아이덴티티 어설션](identity-assertion.md)**에서 다룹니다.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73]
|
||||
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056]
|
||||
tool: 1
|
||||
---
|
||||
# 세션 그룹 {#session-groups}
|
||||
|
|
@ -78,7 +78,7 @@ translation:
|
|||
## 요약 {#recap}
|
||||
|
||||
* `ClientSessionGroup`은 여러 서버 연결을 담고 도구, 리소스, 프롬프트를 각각 하나의 `dict`로 합칩니다.
|
||||
* 서버마다 `connect_to_server(params)`를 호출합니다. `Client`가 받는 서버 객체나 URL이 아니라 트랜스포트 매개변수를 받습니다.
|
||||
* 서버마다 `connect_to_server(params)`를 호출합니다. `Client`가 받는 URL이나 `Transport`가 아니라 트랜스포트 매개변수를 받습니다.
|
||||
* `group.call_tool(name, arguments)`는 소유한 서버로 알아서 라우팅합니다.
|
||||
* 이름은 그룹 전체에서 고유해야 합니다. `search` 도구를 가진 두 서버는 그대로는 공존할 수 없습니다.
|
||||
* `component_name_hook=`은 등록되는 모든 이름을 다시 씁니다. 딕셔너리 키는 바뀌지만 전송되는 이름은 바뀌지 않습니다.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
translation:
|
||||
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c]
|
||||
sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302]
|
||||
tool: 1
|
||||
---
|
||||
# 클라이언트 트랜스포트 {#client-transports}
|
||||
|
|
@ -11,30 +11,15 @@ translation:
|
|||
|
||||
각 트랜스포트의 **서버** 쪽(`mcp.run()`이 하는 일과 배포 대상)은 **[서버 실행하기](../run/index.md)**에서 다룹니다.
|
||||
|
||||
## 인메모리 {#in-memory}
|
||||
|
||||
서버 객체 자체를 전달하세요.
|
||||
|
||||
```python title="client.py" hl_lines="14"
|
||||
--8<-- "docs_src/client_transports/tutorial001.py"
|
||||
```
|
||||
|
||||
서브프로세스도, 포트도, 네트워크를 오가는 바이트도 없습니다. 클라이언트와 서버는 같은 프로세스 안의 두 객체일 뿐이지만, 호출은 여전히 실제 프로토콜 계층을 거칩니다. `search_books`는 HTTP를 통할 때와 똑같이 나열되고, 검증되고, 호출됩니다.
|
||||
|
||||
덕분에 이 방식은 동시에 두 가지 역할을 합니다.
|
||||
|
||||
* **테스트 도구.** 이 문서의 모든 예제는 이 방식으로 실행되며, **[테스트](../get-started/testing.md)** 페이지는 전체 패턴을 이 방식 위에 구축합니다.
|
||||
* **임베딩 API.** 서버를 직접 생성하는 애플리케이션은 도구를 호출하기 위해 네트워크를 거칠 필요가 없습니다.
|
||||
|
||||
## Streamable HTTP {#streamable-http}
|
||||
|
||||
URL 문자열을 전달하면 **Streamable HTTP**를 얻습니다. 배포 시 사용하는 트랜스포트입니다.
|
||||
URL 문자열을 전달하면 **Streamable HTTP**를 사용합니다. 배포할 때 그 뒤에 서버를 두는 트랜스포트이자, 가장 먼저 선택해야 할 트랜스포트입니다.
|
||||
|
||||
```python title="client.py" hl_lines="5"
|
||||
--8<-- "docs_src/client_transports/tutorial002.py"
|
||||
```
|
||||
|
||||
이것이 프로덕션 클라이언트의 전부입니다. `Client`가 URL을 `streamable_http_client(...)`로 감싸 주며, 그 아래에는 MCP에 맞게 설정된 `httpx2.AsyncClient`가 있습니다. `follow_redirects=True`, connect/write/pool에 30초 타임아웃, 그리고 서버가 응답 스트림을 열어 둘 수 있으므로 read에는 300초 타임아웃이 적용됩니다.
|
||||
이것이 프로덕션 클라이언트의 전부입니다. `Client`가 URL을 `streamable_http_client(...)`로 감싸 주며, 그 아래에는 MCP에 맞게 설정된 `httpx2.AsyncClient`가 있습니다. connect/write/pool에는 30초 타임아웃, 그리고 서버가 응답 스트림을 열어 둘 수 있으므로 read에는 300초 타임아웃이 적용됩니다.
|
||||
|
||||
!!! check
|
||||
생성만 한 `Client`는 **연결되지 않은** 상태입니다. 생성은 트랜스포트를 고를 뿐이고,
|
||||
|
|
@ -50,7 +35,7 @@ URL 문자열을 전달하면 **Streamable HTTP**를 얻습니다. 배포 시
|
|||
|
||||
`Authorization` 헤더, 쿠키, 프록시, mTLS, 다른 타임아웃이 필요해지는 순간, `httpx2.AsyncClient`를 직접 만들어 `streamable_http_client`에 넘기세요.
|
||||
|
||||
```python title="client.py" hl_lines="8-14"
|
||||
```python title="client.py" hl_lines="8-13"
|
||||
--8<-- "docs_src/client_transports/tutorial003.py"
|
||||
```
|
||||
|
||||
|
|
@ -80,9 +65,30 @@ TLS 관련 참고 사항이 하나 있습니다. `httpx2`는 번들된 CA 목록
|
|||
!!! info
|
||||
`httpx2`는 익숙한 `httpx` API를 그대로 유지하므로, `httpx`를 안다면 여기서 인증, 프록시,
|
||||
이벤트 훅, 재시도, 연결 제한을 다루는 방법도 이미 아는 셈입니다. SDK는 그 위에 아무것도
|
||||
더하지 않고 아무것도 빼지 않습니다. OAuth가 연결되는 지점도 여기입니다.
|
||||
더하지 않고 아무것도 빼지 않습니다. 단, [리디렉션 처리](#redirects)만은 예외입니다. OAuth가 연결되는 지점도 여기입니다.
|
||||
`httpx2.AsyncClient(auth=OAuthClientProvider(...))`. 전체 흐름은 **[OAuth 클라이언트](oauth-clients.md)**에서 다룹니다.
|
||||
|
||||
### 리디렉션 {#redirects}
|
||||
|
||||
트랜스포트는 전달받은 URL에 연결하며, 그 오리진에만 연결합니다.
|
||||
|
||||
* 같은 스킴, 호스트, 포트 안에 머무는 `307`/`308` 리디렉션은 따라가며, 같은 호스트에서 `http://` → `https://`로 가는 리디렉션도 따라갑니다. 흔한 `/mcp` → `/mcp/` 후행 슬래시 리디렉션이 여기에 포함됩니다.
|
||||
* 그 밖의 곳으로 가는 리디렉션은 따라가지 **않습니다**. 호출은 다음 오류로 실패합니다.
|
||||
|
||||
```text
|
||||
MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server
|
||||
```
|
||||
|
||||
그 URL이 의도한 서버라면 설정에 그 URL을 넣으세요. 아니라면 서버나 그 앞의 프록시 설정이 잘못된 것입니다.
|
||||
|
||||
이 규칙은 전달하는 어떤 `httpx2.AsyncClient`에도 적용됩니다. MCP 요청에는 `follow_redirects` 설정이 어느 방향으로든 참조되지 않습니다. SDK의 OAuth 프로바이더도 자체 요청에 같은 규칙을 적용합니다.
|
||||
|
||||
!!! tip
|
||||
`Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP`는
|
||||
서버가 자신이 모르는 TLS 종료 프록시 뒤에 있어 `http://` 리디렉션을 내보내고 있다는 뜻입니다.
|
||||
이는 서버 쪽에서 고치거나(**[배포와 확장](../run/deploy.md#behind-a-tls-terminating-proxy)**),
|
||||
메시지가 제안하는 정확한 `https://…/` URL을 사용해 해결합니다.
|
||||
|
||||
## stdio {#stdio}
|
||||
|
||||
**stdio** 서버는 서브프로세스입니다. 클라이언트가 이를 실행하고, stdin에 JSON-RPC를 쓰고, stdout에서 JSON-RPC를 읽습니다. 데스크톱 호스트가 사용자 컴퓨터에서 서버를 실행하는 방식이 바로 이것입니다. 호스트는 **곧** 이 코드에 UI를 더한 것이며, **[실제 호스트에 연결하기](../get-started/real-host.md)**는 같은 관계를 호스트 쪽에서 설정 파일로 바라본 것입니다.
|
||||
|
|
@ -105,6 +111,18 @@ TLS 관련 참고 사항이 하나 있습니다. `httpx2`는 번들된 CA 목록
|
|||
API 키가 필요한 서버는 거기서 키를 찾지 못합니다. `env=`로 명시적으로 전달하세요. 해당
|
||||
변수는 허용 목록 위에 병합됩니다. 위 예제에서 `BOOKSHOP_API_KEY`가 하는 일이 바로 이것입니다.
|
||||
|
||||
## 인메모리 {#in-memory}
|
||||
|
||||
테스트에서는 배포할 것도, 실행할 것도 없습니다. 서버 객체 자체를 전달하세요.
|
||||
|
||||
```python hl_lines="14"
|
||||
--8<-- "docs_src/client_transports/tutorial001.py"
|
||||
```
|
||||
|
||||
서브프로세스도, 포트도, 네트워크를 오가는 바이트도 없습니다. 클라이언트와 서버는 같은 프로세스 안의 두 객체이며, 호출은 여전히 실제 프로토콜 계층을 거칩니다. `search_books`는 HTTP를 통할 때와 똑같이 나열되고, 검증되고, 호출됩니다. **[테스트](../get-started/testing.md)** 페이지는 전체 패턴을 이 방식 위에 구축합니다.
|
||||
|
||||
같은 형태가 임베딩 API 역할도 겸합니다. 서버를 직접 생성하는 애플리케이션은 네트워크를 거치지 않고 서버의 도구를 호출할 수 있습니다.
|
||||
|
||||
## SSE {#sse}
|
||||
|
||||
`mcp.client.sse`의 `sse_client(url)`은 Streamable HTTP로 대체된 이전 HTTP 트랜스포트입니다. 아직 이 방식을 쓰는 서버와 통신하려면 `Client(sse_client("http://localhost:8000/sse"))`처럼 같은 방식으로 감싸서 사용하되, 새로운 것을 이 위에 만들지는 마세요.
|
||||
|
|
@ -113,15 +131,16 @@ TLS 관련 참고 사항이 하나 있습니다. `httpx2`는 번들된 CA 목록
|
|||
|
||||
`Client`에게 위의 모든 것은 같은 것입니다.
|
||||
|
||||
**트랜스포트**란 `(read, write)` 메시지 스트림 쌍을 내어주는 비동기 컨텍스트 매니저라면 무엇이든 해당합니다. 정식으로는 `mcp.client`의 `Transport` 프로토콜입니다. `Client`는 인자를 타입으로 구분합니다. 서버 객체는 프로세스 내에서 연결하고, `str`은 `streamable_http_client(url)`이 되며, `StdioServerParameters`는 `stdio_client(params)`가 되고, 그 밖의 것은 트랜스포트로 직접 진입합니다. 마지막 규칙 덕분에 `stdio_client(...)`, `streamable_http_client(...)`, `sse_client(...)`가 모두 같은 자리에 들어가고, 직접 만든 트랜스포트도 쓸 수 있습니다.
|
||||
**트랜스포트**란 `(read, write)` 메시지 스트림 쌍을 내어주는 비동기 컨텍스트 매니저라면 무엇이든 해당합니다. 정식으로는 `mcp.client`의 `Transport` 프로토콜입니다. `Client`는 인자를 타입으로 구분합니다. `str`은 `streamable_http_client(url)`이 되고, `StdioServerParameters`는 `stdio_client(params)`가 되며, 서버 객체는 프로세스 내에서 연결하고, 그 밖의 것은 트랜스포트로 직접 진입합니다. 마지막 규칙 덕분에 `stdio_client(...)`, `streamable_http_client(...)`, `sse_client(...)`가 모두 같은 자리에 들어가고, 직접 만든 트랜스포트도 쓸 수 있습니다.
|
||||
|
||||
## 요약 {#recap}
|
||||
|
||||
* `Client(mcp)`(서버 객체)는 인메모리로 연결합니다. 테스트와 임베딩에 사용하세요.
|
||||
* `Client("http://.../mcp")`(URL)는 프로덕션 트랜스포트인 Streamable HTTP로 연결합니다.
|
||||
* 헤더, 인증, 프록시, 타임아웃은 `streamable_http_client(url, http_client=...)`에 전달하는 `httpx2.AsyncClient`에 설정합니다. `headers=` 키워드는 없습니다.
|
||||
* 리디렉션은 URL 자체의 오리진 안에서만(후행 슬래시 `307`/`308`) 따라가며, 같은 호스트의 `http`→`https`도 따라갑니다. 그 밖의 경우는 `Redirect to … not followed`로 실패하므로 최종 URL을 설정하세요.
|
||||
* stdio는 `Client(StdioServerParameters(...))`입니다. 자식 프로세스의 stderr를 다른 곳으로 돌릴 때만 직접 `stdio_client(...)`로 감싸세요.
|
||||
* 서브프로세스는 현재 환경이 아니라 허용 목록에 있는 환경 변수만 받습니다. `env=`로 여기에 추가합니다.
|
||||
* `Client(mcp)`(서버 객체)는 인메모리로 연결합니다. 테스트에서 사용하거나, 서버를 만든 애플리케이션 안에 서버를 임베딩할 때 사용하세요.
|
||||
* 트랜스포트는 `async with x as (read, write)`로 쓸 수 있는 것이면 무엇이든 됩니다. `Client`는 서버 객체, URL, `StdioServerParameters`가 아닌 것은 모두 그 프로토콜에 그대로 넘깁니다.
|
||||
* `Client`를 생성하면 트랜스포트가 정해집니다. `async with`가 이를 엽니다.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue