docs: publish translated docs in twelve languages and the tool that maintains them (#3280)
This commit is contained in:
parent
14aa889992
commit
52ad0a8876
6 changed files with 89737 additions and 54 deletions
123
i18n/ko/pages/advanced/apps.md
Normal file
123
i18n/ko/pages/advanced/apps.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
---
|
||||
translation:
|
||||
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
|
||||
tool: 1
|
||||
---
|
||||
# MCP Apps {#mcp-apps}
|
||||
|
||||
**MCP App**은 얼굴을 가진 도구입니다. 도구가 데이터와 함께 HTML 문서를 가리키면, 호스트는 이 문서를 상호작용 가능한 화면으로 렌더링합니다.
|
||||
|
||||
두 부분으로 이루어지며, 언제나 두 부분입니다.
|
||||
|
||||
1. 다른 도구와 마찬가지로 작업을 수행하고 데이터를 반환하는 **도구**.
|
||||
2. 호스트가 도구를 위해 보여 줄 HTML을 담은 **`ui://` 리소스**.
|
||||
|
||||
도구는 리소스를 가리키는 `_meta.ui.resourceUri` 참조를 지닙니다. 호스트는 `resources/read`로 리소스를 가져와 **샌드박스 처리된 iframe**에 렌더링하고, 도구의 결과를 `postMessage`로 그 iframe에 전달합니다. 서버는 어떤 `ui/*` 메시지도 주고받지 않습니다. 그 트래픽은 호스트와 iframe 사이의 일입니다. 서버는 도구와 HTML 문서를 제공할 뿐이고, 나머지 연출은 호스트가 맡습니다.
|
||||
|
||||
SDK는 이를 내장 `Apps` 확장(`io.modelcontextprotocol/ui`)으로 제공합니다. [확장](extensions.md)이 처음이라면 먼저 그 페이지를 훑어보세요. 1분이면 충분하니 읽고 돌아오면 됩니다.
|
||||
|
||||
## 얼굴을 가진 시계 {#a-clock-with-a-face}
|
||||
|
||||
```python title="server.py" hl_lines="19 22 30 32"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
네 가지 단계입니다.
|
||||
|
||||
* `Apps()`: 인스턴스 하나가 UI에 연결된 도구와 그 리소스를 모두 담습니다.
|
||||
* `@apps.tool(resource_uri="ui://clock/app.html")`: 일반 도구에 `_meta.ui.resourceUri` 표시가 더해집니다. `@mcp.tool()`이 받는 모든 것(name, title, description, ...)이 그대로 전달됩니다.
|
||||
* `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)`: 짝이 되는 리소스이며 `text/html;profile=mcp-app`으로 제공됩니다. 바로 이 MIME 타입이 호스트에게 "이것은 앱이니 렌더링하라"고 알려 줍니다.
|
||||
* `MCPServer("clock", extensions=[apps])`: 옵트인입니다. 이제 서버는 `capabilities.extensions` 아래에 `io.modelcontextprotocol/ui`를 알립니다.
|
||||
|
||||
HTML 자체는 호스트의 `postMessage`를 수신하고 결과를 표시합니다. 실제 앱에서는 HTML 안에서 공식 [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) 브라우저 SDK를 사용하세요. 원시 메시지 이벤트 대신 `ontoolresult`, `callServerTool`, `getHostContext`, `onhostcontextchanged`를 제공합니다.
|
||||
|
||||
## 우아한 성능 저하 {#graceful-degradation}
|
||||
|
||||
모든 클라이언트가 앱을 렌더링하지는 않습니다. 이것이 서버에 어떤 의미인지 사양은 분명하게 말합니다.
|
||||
|
||||
> 도구는 UI를 사용할 수 있는 경우에도 의미 있는 `content` 배열을 반환해야 **합니다(MUST)**.
|
||||
|
||||
모델은 `content`를 읽고, iframe은 사람을 위한 것입니다. UI를 지원하는 호스트도 여전히 텍스트 결과를 모델에 전달하며, 텍스트 전용 클라이언트는 **오직** 그 텍스트만 받습니다. 따라서 표준 패턴은 도구 하나에 답 둘입니다. `get_time`을 다시 살펴보세요.
|
||||
|
||||
```python title="server.py" hl_lines="23-27"
|
||||
--8<-- "docs_src/apps/tutorial001.py"
|
||||
```
|
||||
|
||||
`client_supports_apps(ctx)`는 클라이언트가 `io.modelcontextprotocol/ui` 확장을 선언했고 **동시에** `mimeTypes` 설정에 `text/html;profile=mcp-app`을 나열했을 때만 `True`입니다. 이 필드는 필수이므로 생략한 클라이언트는 해당하지 않습니다. 같은 파일의 `main()`이 선언하는 것이 바로 이것입니다. 협상의 클라이언트 쪽 절반을 선언하면 풍부한 답이 돌아옵니다.
|
||||
|
||||
!!! warning
|
||||
`"[Rendered UI]"` 같은 자리 표시자를 유일한 content로 반환하지 마세요. 대체 텍스트가 쓸모없다면, 그 도구는 모든 텍스트 전용 클라이언트와 모델 자체에 쓸모없는 도구가 됩니다. 제대로 된 문장을 작성하세요.
|
||||
|
||||
## iframe 잠그기 {#locking-the-iframe-down}
|
||||
|
||||
리소스 쪽이 보안 메타데이터를 지닙니다. iframe이 무엇을 로드할 수 있는지, 어떤 브라우저 권한을 원하는지, 어떻게 프레임에 담기기를 원하는지를 담습니다.
|
||||
|
||||
```python title="server.py" hl_lines="9 19-22"
|
||||
--8<-- "docs_src/apps/tutorial002.py"
|
||||
```
|
||||
|
||||
`csp`와 `permissions`는 서버의 동작이 아니라 **호스트에 대한 요청**입니다. 호스트는 이를 바탕으로 iframe의 Content-Security-Policy와 Permissions-Policy를 구성하며, 거부할 수도 있습니다. 허가되었다고 가정하지 말고 JS에서 기능 탐지를 하세요.
|
||||
|
||||
`ResourceCsp`를 필드별로 살펴보면 다음과 같습니다(Python 이름, 와이어 키, 호스트가 이것으로 하는 일).
|
||||
|
||||
| Python | 와이어 (`_meta.ui.csp`) | 제어 대상 |
|
||||
|---|---|---|
|
||||
| `connect_domains` | `connectDomains` | `connect-src`: `fetch`/XHR이 갈 수 있는 곳 |
|
||||
| `resource_domains` | `resourceDomains` | `img-src`, `style-src`, ...: 정적 자산 |
|
||||
| `frame_domains` | `frameDomains` | `frame-src`: 중첩 iframe |
|
||||
| `base_uri_domains` | `baseUriDomains` | `base-uri`: `<base>`가 가리킬 수 있는 곳 |
|
||||
|
||||
`ResourcePermissions`: 각 필드는 iframe을 위한 브라우저 권한 하나를 요청합니다.
|
||||
|
||||
| Python | 와이어 (`_meta.ui.permissions`) |
|
||||
|---|---|
|
||||
| `camera` | `camera` |
|
||||
| `microphone` | `microphone` |
|
||||
| `geolocation` | `geolocation` |
|
||||
| `clipboard_write` | `clipboardWrite` |
|
||||
|
||||
!!! note
|
||||
CSP와 권한은 **리소스**에 있으며, 도구에는 절대 두지 않습니다. 사양의 도구 메타데이터에는 이를 위한 자리가 없고, 호스트는 그곳에 있는 값을 무시합니다. SDK는 이 실수를 아예 표현할 수 없게 만듭니다. `@apps.tool()`에는 `csp` 매개변수가 없습니다.
|
||||
|
||||
### 가시성 {#visibility}
|
||||
|
||||
도구의 `visibility=["app"]`은 "이것은 모델이 아니라 iframe을 위해 존재한다"는 뜻입니다.
|
||||
|
||||
* `"model"`: 모델이 호출할 수 있습니다.
|
||||
* `"app"`: iframe이 호출할 수 있습니다(`callServerTool`을 통해).
|
||||
* 생략: 둘 다이며, 이것이 기본값입니다.
|
||||
|
||||
필터링은 **호스트**의 일입니다. 서버는 앱 전용 도구를 다른 도구와 마찬가지로 `tools/list`에 나열하고, 호스트가 이를 모델에게서 숨깁니다. 서버 쪽에서 필터링하지 마세요.
|
||||
|
||||
## SDK가 강제하는 규칙 {#the-rules-the-sdk-enforces}
|
||||
|
||||
모두 프로덕션이 아니라 시작 시점에 실패합니다.
|
||||
|
||||
* `ui://...`가 아닌 `resource_uri`나 리소스 URI는 데코레이션/등록 시점에 `ValueError`입니다.
|
||||
* **짝이 되는 등록된 리소스가 없는** URI에 연결된 도구는 `MCPServer(extensions=[apps])`가 확장을 소비할 때 `ValueError`입니다. `resources/read`에서 404가 나는 HTML을 알리는 도구는 잘못된 설정이므로, 생성 자체를 거부합니다.
|
||||
* `@apps.tool()`의 `meta={"ui": ...}`는 `ValueError`입니다. `_meta["ui"]`는 데코레이터의 소유이니 `resource_uri=`와 `visibility=`로 표현하세요. 다른 `meta=` 키는 문제없이 함께 병합됩니다.
|
||||
|
||||
TypeScript ext-apps SDK도 FastMCP도 현재는 이 중 어느 것도 잡아내지 못합니다. 호스트가 발견하기 전에 먼저 알게 되는 편이 낫다고 생각합니다.
|
||||
|
||||
## 인라인 HTML 너머 {#beyond-inline-html}
|
||||
|
||||
`add_html_resource`는 흔한 경우, 즉 HTML 문자열을 다룹니다. 그 밖의 경우, 디스크에 있는 HTML이나 생성된 콘텐츠라면 리소스를 직접 만들어 넘기세요.
|
||||
|
||||
```python title="server.py" hl_lines="12 18"
|
||||
--8<-- "docs_src/apps/tutorial003.py"
|
||||
```
|
||||
|
||||
`add_resource`는 리소스가 MIME 타입을 명시적으로 설정하지 않았을 때 `text/html;profile=mcp-app`을 채워 넣고, 명시적으로 불일치하는 값은 거부합니다. 다른 MIME 타입의 `ui://` 리소스는 어떤 호스트도 렌더링하지 않기 때문입니다.
|
||||
|
||||
!!! tip
|
||||
지원 중단 예정(deprecated)인 평면 키 `_meta["ui/resourceUri"]`를 여전히 읽는 GA 이전 호스트를 대상으로 하나요? 직접 병합하세요.
|
||||
`@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})`.
|
||||
중첩된 `ui` 객체가 사양의 형태이며, 평면 키는 사라지는 중입니다.
|
||||
|
||||
## 실행해 보기 {#see-it-run}
|
||||
|
||||
`examples/stories/`의 `apps` 스토리는 이 페이지를 실행 가능한 한 쌍으로 만든 것입니다. UI에 연결된 시계 도구를 갖춘 서버, 그리고 Apps를 협상하고 도구의 `_meta.ui.resourceUri`를 읽고 HTML을 가져와 도구를 호출하는 클라이언트입니다.
|
||||
|
||||
```bash
|
||||
uv run python -m stories.apps.client
|
||||
```
|
||||
172
i18n/ko/pages/advanced/extensions.md
Normal file
172
i18n/ko/pages/advanced/extensions.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
---
|
||||
translation:
|
||||
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
|
||||
tool: 1
|
||||
---
|
||||
# 확장 {#extensions}
|
||||
|
||||
**확장**은 하나의 식별자 아래에 묶어 두고 원할 때만 켜서 쓰는 MCP 동작의 묶음입니다.
|
||||
|
||||
서버에서는 도구, 리소스, 새로운 요청 메서드를 제공할 수 있고 `tools/call`을 감쌀 수 있습니다. 클라이언트에서는 추가적인 `tools/call` 결과 형태를 클레임하고 벤더 알림을 관찰할 수 있습니다. 양쪽 모두 각자의 `capabilities.extensions` 아래에 이를 광고하며, 요청하지 않은 쪽에는 아무것도 달라지지 않습니다. 이것이 계약([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133))이고, 황금률은 단 하나입니다. **확장은 기본적으로 꺼져 있습니다**.
|
||||
|
||||
## 확장 사용하기 {#using-an-extension}
|
||||
|
||||
생성할 때 인스턴스를 전달하세요.
|
||||
|
||||
```python title="server.py"
|
||||
--8<-- "docs_src/extensions/tutorial001.py"
|
||||
```
|
||||
|
||||
끝입니다. 이제 서버는 `capabilities.extensions` 아래에 `io.modelcontextprotocol/ui`를 광고하고 확장이 제공하는 모든 것을 서비스합니다.
|
||||
|
||||
`Apps`는 내장된 참조 확장이며 별도의 페이지에서 다룹니다. **[MCP Apps](apps.md)**를 참고하세요.
|
||||
|
||||
!!! note
|
||||
확장은 생성 시점에 고정됩니다. 나중에 호출할 `add_extension`은 없습니다. 클라이언트가 연결되어 있는 동안 서버의 기능 맵이 바뀌어서는 안 되기 때문입니다.
|
||||
|
||||
기능 맵은 `server/discover`에 실려 전달되는데, 이는 **2026-07-28** 경로입니다. 레거시 `initialize` 핸드셰이크에는 이를 담을 자리가 없으므로 레거시 클라이언트는 확장을 아예 보지 못합니다. 이를 고려해 설계하세요. 확장은 서버를 **보강**하는 것이지, 서버를 사용할 수 있는 유일한 방법이 되어서는 안 됩니다.
|
||||
|
||||
## 직접 작성하기 {#writing-your-own}
|
||||
|
||||
`Extension`을 서브클래싱하고 필요한 것만 재정의하세요. 모든 메서드에는 기본 구현이 있습니다.
|
||||
|
||||
### 식별자 {#the-identifier}
|
||||
|
||||
```python
|
||||
--8<-- "docs_src/extensions/tutorial002.py"
|
||||
```
|
||||
|
||||
식별자는 사양의 `_meta` 키 문법을 따르는 `vendor-prefix/name` 문자열입니다. 점으로 구분된 레이블(각각 문자로 시작하고 문자 또는 숫자로 끝남), 슬래시, 그리고 이름 순서입니다. **클래스가 정의될 때** 검증되므로 오타가 서버 부팅까지 숨어 있지 않습니다.
|
||||
|
||||
```text
|
||||
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
|
||||
(reverse-DNS prefix required), got 'stamps'
|
||||
```
|
||||
|
||||
직접 관리하는 도메인을 접두사로 사용하세요. `io.modelcontextprotocol/*`는 MCP 프로젝트 자체가 규정하는 확장을 위한 것입니다.
|
||||
|
||||
### 도구 제공하기 {#contributing-tools}
|
||||
|
||||
쓸모 있는 가장 작은 확장은 도구 하나와 설정 맵 하나입니다.
|
||||
|
||||
```python title="server.py" hl_lines="17 19-20 22-23 26"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
* `tools()`는 `ToolBinding`을 반환합니다. 서버는 각각을 직접 `mcp.add_tool(...)`을 호출한 것과 똑같이 등록합니다. 스키마 생성도, `Context` 주입도, 나머지도 모두 같습니다.
|
||||
* `settings()`는 `capabilities.extensions["com.example/stamps"]`에 광고되는 값입니다. 설정 없이 확장을 광고하려면 `{}`(기본값)을 반환하세요.
|
||||
* 확장은 서버를 절대 전달받지 않습니다. 기여할 내용을 데이터로 선언하고, `MCPServer`가 이를 소비합니다. 변경할 `self.server` 같은 것은 없습니다.
|
||||
|
||||
그리고 `main()`이 그 증거입니다. `mcp`에 바로 연결하는 인메모리 클라이언트입니다.
|
||||
|
||||
```python title="server.py" hl_lines="29-34"
|
||||
--8<-- "docs_src/extensions/tutorial003.py"
|
||||
```
|
||||
|
||||
### 자체 메서드 제공하기 {#serving-your-own-methods}
|
||||
|
||||
확장은 **새로운 요청 메서드**를 등록할 수 있습니다. 사양의 동사 옆에서 함께 서비스되는 자체 동사입니다.
|
||||
|
||||
```python title="server.py" hl_lines="16-22 31 40-48"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `SearchParams`는 `RequestParams`를 서브클래싱하므로 2026 `_meta` 봉투가 일관되게 파싱되고, 핸들러는 원시 dict가 아니라 검증된 params를 받습니다. 클라이언트가 제어하는 값에는 한계를 두세요. `Field(ge=1, le=100)`은 코드가 무언가를 할당하기 전에 터무니없는 `limit`을 거부합니다.
|
||||
* `require_client_extension(ctx, EXTENSION_ID)`가 관문입니다. 확장을 선언하지 않은 클라이언트는 사양이 요구하는 기계 판독 가능한 `requiredCapabilities` 페이로드와 함께 `-32021`(필수 클라이언트 기능 누락) 오류를 받습니다.
|
||||
* `protocol_versions=frozenset({"2026-07-28"})`은 메서드를 하나의 와이어 버전에 고정합니다. 다른 버전에서는 클라이언트가 `METHOD_NOT_FOUND`를 받는데, 그 버전에 메서드가 존재하지 않는 것과 똑같습니다. 그 클라이언트에게는 실제로 존재하지 않는 셈입니다.
|
||||
|
||||
메서드는 **엄격하게 추가만 가능**합니다. SDK는 이를 런타임이 아니라 생성 시점에 강제합니다.
|
||||
|
||||
* 사양에 정의된 메서드(`tools/list`, `completion/complete`, ...)에 대한 `MethodBinding`은 바인딩이 생성될 때 `ValueError`를 일으킵니다. 핵심 동사는 서버의 것입니다.
|
||||
* 두 확장이 같은 메서드를 바인딩하면 두 번째가 등록될 때 오류가 납니다. 마지막 쓰기가 이기는 방식은 플러그인이 서로를 망가뜨리는 원인이므로 그렇게 하지 않습니다.
|
||||
* 빈 `protocol_versions` 집합도 오류를 일으킵니다. 절대 서비스될 수 없는 메서드는 설정이 아니라 버그입니다.
|
||||
|
||||
### 클라이언트 측 {#the-client-side}
|
||||
|
||||
같은 파일의 `main()`이 클라이언트 쪽 이야기의 전부이며, 두 부분을 모두 담고 있습니다.
|
||||
|
||||
```python title="server.py" hl_lines="54-58"
|
||||
--8<-- "docs_src/extensions/tutorial004.py"
|
||||
```
|
||||
|
||||
* `Client(..., extensions=[advertise(EXTENSION_ID)])`가 확장을 선언합니다. 선언은 `ClientCapabilities.extensions`가 됩니다. 2026-07-28 연결에서는 이 맵이 요청별 `_meta` 봉투에 실려 이동하므로 서버는 **모든** 요청에서 이를 봅니다. 레거시 연결에서는 `initialize` 핸드셰이크에 실립니다. 서버 코드는 어느 쪽인지 신경 쓰지 않습니다. `require_client_extension(ctx, ...)`와 `ctx.session.check_client_capability(...)`는 두 경로 모두에서 올바른 출처를 읽습니다.
|
||||
* 벤더 메서드는 한 계층 아래인 `client.session.send_request(...)`로 내려갑니다. `Client`는 사양 동사에 대해서만 일급 메서드를 갖춥니다. `send_request`는 모든 `Request` 서브클래스를 받으므로 벤더 요청은 그대로 통과합니다.
|
||||
|
||||
### `tools/call` 가로채기 {#intercepting-toolscall}
|
||||
|
||||
유일하게 개입하는 훅입니다. 도구 호출을 관찰하거나, 단락시키거나, 거부하려면 `intercept_tool_call`을 재정의하세요.
|
||||
|
||||
```python title="server.py" hl_lines="17-24"
|
||||
--8<-- "docs_src/extensions/tutorial005.py"
|
||||
```
|
||||
|
||||
* `params`는 검증된 `CallToolRequestParams`입니다. 원시 JSON을 건드리지 않고 `params.name`과 `params.arguments`를 얻습니다. 어느 도구 호출이 실행될지 결정하는 것도 바로 이것입니다. 다시 작성한 컨텍스트를 `call_next`에 넘기면 핸들러가 `ctx`에서 관찰하는 내용이 바뀔 뿐 도구 호출 자체는 바뀌지 않습니다. 와이어 수준의 요청 재작성은 [미들웨어](middleware.md)의 몫입니다.
|
||||
* `call_next(ctx)`는 체인의 나머지를 실행하고 핸들러의 결과를 반환합니다. 그대로 반환하거나(관찰), 다른 것을 반환하거나(대체), `MCPError`를 일으키세요(거부). 무엇을 반환하든 2026 계열의 `serverInfo` 신원 스탬프를 포함해 다른 핸들러 결과와 똑같이 직렬화되므로, 단락시키는 인터셉터가 익명이거나 스키마에 어긋나는 응답을 만들어 내는 일은 없습니다.
|
||||
* 확장이 여럿이면 인터셉터는 등록 순서대로 중첩됩니다. `extensions=[...]`의 첫 번째 확장이 가장 바깥쪽입니다.
|
||||
* 기본 구현은 그대로 통과시키며, 확장이 이 훅을 재정의하지 않는 서버는 원래의 `tools/call` 핸들러를 그대로 유지합니다. 쓰지 않는 것에는 비용을 치르지 않습니다.
|
||||
|
||||
이 훅은 `tools/call`만 감싸고 다른 것은 감싸지 않습니다. 모든 메시지에 걸친 관심사에는 [미들웨어](middleware.md)를 사용하세요. 미들웨어는 바로 그런 용도입니다.
|
||||
|
||||
## 클라이언트 확장 사용하기 {#using-a-client-extension}
|
||||
|
||||
**클라이언트 확장**은 소비하는 쪽에서 본 같은 계약으로, 하나의 식별자 아래 묶인 클라이언트 측 동작의 묶음입니다. `Client(extensions=[...])`에 인스턴스를 전달하고 평소처럼 도구를 호출하세요.
|
||||
|
||||
```python title="client.py" hl_lines="66-68"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
`call_tool("buy", ...)`은 다른 모든 호출처럼 평범한 `CallToolResult`를 반환합니다. 확장이 바꾼 것은 이렇습니다. 이제 서버는 `buy`에 최종 결과 대신 `receipt` **결과 형태**로 응답할 수 있고, `call_tool`이 반환하기 전에 `Receipts`가 이를 마무리합니다(여기서는 후속 호출로 영수증을 정산합니다). 호출 지점에서는 아무것도 달라지지 않습니다.
|
||||
|
||||
확장을 빼면 이 중 어떤 것도 존재하지 않습니다. 서버의 관문은 확장을 선언하지 않은 클라이언트를 거부하고(오류 -32021), 관문을 건너뛰는 서버가 보낸 클레임된 형태는 인식되지 않은 `resultType`에 대해 사양이 요구하는 그대로 검증에 실패합니다. 와이어 양 끝 모두에서 기본적으로 꺼져 있습니다.
|
||||
|
||||
클라이언트 측 동작이 **전혀 없는** 식별자를 광고하려면(위의 검색 클라이언트처럼 서버는 기능을 기준으로 관문을 두고 클라이언트는 아무것도 하지 않는 경우) `advertise()`를 사용하세요.
|
||||
|
||||
```python
|
||||
from mcp.client import advertise
|
||||
|
||||
client = Client(mcp, extensions=[advertise("com.example/search")])
|
||||
```
|
||||
|
||||
## 클라이언트 확장 작성하기 {#writing-a-client-extension}
|
||||
|
||||
`ClientExtension`을 서브클래싱하고 필요한 것만 재정의하세요. 기여 종류는 세 가지이며 각각 기본 구현이 있습니다. `settings()`, `claims()`, `notifications()`입니다.
|
||||
|
||||
```python title="client.py" hl_lines="17-18 43-44 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial006.py"
|
||||
```
|
||||
|
||||
* 식별자는 서버의 것과 같은 문법을 따르며 클래스가 정의될 때 검증됩니다.
|
||||
* `claims()`는 `ResultClaim`을 반환합니다. 와이어 태그, 이를 파싱하는 모델, 마무리하는 리졸버로 구성됩니다. 모델은 `result_type: Literal["receipt"]`으로 태그를 고정해야 하며 해당 동사의 핵심 결과 타입을 서브클래싱해서는 안 됩니다. 둘 다 클레임이 생성될 때 강제됩니다. `receipt_token` 같은 벤더 필드는 와이어에 그대로 실립니다. 대체된 형태는 클라이언트에 원문 그대로 도달합니다.
|
||||
* 리졸버는 파싱된 모델과 `ClaimContext`를 받습니다. `ctx.session`은 `client.session`과 같은 공개 핸들이므로 후속 작업은 평범한 세션 호출입니다. 리졸버는 해당 동사의 일반적인 `CallToolResult`를 반환합니다.
|
||||
* `settings()`는 `ClientCapabilities.extensions[identifier]`에 광고되는 값이며 `Client` 생성 시 한 번 읽힙니다.
|
||||
|
||||
`notifications()`는 관찰할 벤더 서버 알림을 선언합니다.
|
||||
|
||||
```python
|
||||
def notifications(self) -> Sequence[NotificationBinding[Any]]:
|
||||
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
|
||||
```
|
||||
|
||||
핸들러는 검증된 params를 디스패치 순서대로 하나씩 받습니다. 관찰만 할 뿐, 거부하거나 응답할 수는 없습니다.
|
||||
|
||||
조용한 규칙이 두 가지 있습니다. 클레임은 2026-07-28 연결에서만 활성화되며 기능 광고도 이를 따릅니다. 레거시 연결에서는 클레임이 사라지고 식별자도 함께 광고에서 빠지므로, 클라이언트가 스스로 거부할 형태의 확장을 광고하는 일은 없습니다. 그리고 리졸버 대신 클레임된 형태를 직접 받고 싶다면 `client.session.call_tool(..., allow_claimed=True)`를 호출하세요. 이 플래그가 없으면 세션 계층 호출자에게 도달한 클레임된 형태는 `UnexpectedClaimedResult`를 일으킵니다.
|
||||
|
||||
### 확장 동사 {#extension-verbs}
|
||||
|
||||
확장의 자체 요청 메서드에는 클라이언트 측 등록이 필요 없습니다. 벤더 요청 타입은 `mcp.types.Request`를 서브클래싱하고, [자체 메서드 제공하기](#serving-your-own-methods)에서처럼 `client.session.send_request`를 거칩니다. 한 가지가 더 있습니다. params 키가 `Mcp-Name` 헤더에 실려야 할 때(tasks 같은 확장 사양은 자신의 동사에 이를 요구합니다) 요청 타입이 `name_param`을 선언합니다.
|
||||
|
||||
```python title="client.py" hl_lines="22-25 46-47"
|
||||
--8<-- "docs_src/extensions/tutorial007.py"
|
||||
```
|
||||
|
||||
세션은 모든 전송 경로에서 `params["jobId"]`를 `Mcp-Name`에 반영하며, 값이 없으면 필수 헤더를 조용히 빠뜨리는 대신 명시적으로 실패합니다.
|
||||
|
||||
## 확장이 할 수 없는 것 {#what-an-extension-cannot-do}
|
||||
|
||||
기여 범위는 의도적으로 **닫혀** 있습니다. 서버에서는 설정, 도구, 리소스, 메서드, `tools/call` 인터셉터 하나입니다. 클라이언트에서는 설정, 결과 클레임, 알림 바인딩입니다. 확장은 다음을 할 수 없습니다.
|
||||
|
||||
* **호스트 내부에 손대기.** 데이터를 선언할 뿐, 서버나 클라이언트 참조를 쥐지 않습니다.
|
||||
* **핵심 동작 바꾸기.** 사양 메서드와 핵심 결과 태그는 생성 시점에 거부되며(`initialize`는 러너가 아예 예약해 둡니다), 핵심 어휘에 가려지는 알림 바인딩은 대신 경고와 함께 조용해집니다.
|
||||
* **늦게 등록하기.** `MCPServer(...)`나 `Client(...)`가 반환된 뒤에는 확장 집합이 그대로 확정됩니다.
|
||||
|
||||
이 벽과 씨름하고 있다면 확장을 작성하는 것이 아니라 포크를 작성하는 것입니다. 벽이 곧 기능입니다. `extensions=[Apps(), Stamps()]`라는 코드를 읽는 사용자는 그 둘이 건드렸을 수 있는 **모든 것**을 압니다.
|
||||
22
i18n/ko/pages/advanced/index.md
Normal file
22
i18n/ko/pages/advanced/index.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
translation:
|
||||
sections: [ca6988b7503cd2d3]
|
||||
tool: 1
|
||||
---
|
||||
# 고급 {#advanced}
|
||||
|
||||
일반적인 서버나 클라이언트에 필요한 모든 것은 위의 섹션에서 주제별로 다룹니다. 이 섹션은 `MCPServer`의 편의 계층이 오히려 방해가 될 때 꺼내 쓰는 비상구입니다.
|
||||
|
||||
* **[저수준 Server](low-level-server.md)**: `MCPServer`가 기반으로 삼는 클래스입니다. 손으로 작성하는 스키마, `on_*` 핸들러, 아무것도 대신 검사해 주지 않는 구조, 그리고 직접 정의하는 커스텀 JSON-RPC 메서드를 다룹니다.
|
||||
* **[페이지네이션](pagination.md)**과 **[미들웨어](middleware.md)**: 저수준 `Server`에서**만** 할 수 있는 두 가지입니다.
|
||||
* **[확장](extensions.md)**과 **[MCP Apps](apps.md)**: 프로토콜의 확장 지점입니다. 확장 패키지를 서버에 조합해 넣거나 직접 작성할 수 있습니다.
|
||||
|
||||
여기서 찾을 법한 몇 가지 항목은 실제로 사용하는 곳에 배치되어 있습니다.
|
||||
|
||||
* **인가**는 **[서버 실행하기](../run/index.md)** 아래에 있습니다. 서버는 배포하는 곳에서 보호하기 때문입니다.
|
||||
* **OAuth**, **신원 어설션**, **여러 서버**에 연결하기, 응답 **캐시**는 모두 **[클라이언트](../client/index.md)** 아래에 있습니다.
|
||||
* **다중 왕복 요청**과 **구독**은 **[핸들러 내부](../handlers/index.md)** 아래에 있습니다. 둘 다 핸들러가 **수행하는** 일이기 때문입니다.
|
||||
* **URI 템플릿**은 **[서버](../servers/index.md)** 아래, 리소스 옆에 있습니다.
|
||||
* **[프로토콜 버전](../protocol-versions.md)**과 **[지원 중단 예정 기능](../deprecated.md)**은 각각 별도의 최상위 페이지가 있습니다.
|
||||
|
||||
이 섹션이 필요한지 확신이 서지 않는다면, 필요하지 않은 것입니다.
|
||||
207
i18n/ko/pages/advanced/low-level-server.md
Normal file
207
i18n/ko/pages/advanced/low-level-server.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
---
|
||||
translation:
|
||||
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
|
||||
tool: 1
|
||||
---
|
||||
# 저수준 Server {#the-low-level-server}
|
||||
|
||||
`@mcp.tool()`은 하나의 계층입니다. 그 아래에는 두 번째 서버 클래스인 `Server`가 있으며, 이 클래스는 MCP를 날것 그대로 다룹니다. 프로토콜 객체를 넘기면 변경 없이 그대로 와이어에 실어 보냅니다.
|
||||
|
||||
`MCPServer`는 그 위에 만들어져 있습니다. 편의 계층이 방해가 될 때 저수준으로 내려갑니다.
|
||||
|
||||
* Python 시그니처에서 도출한 스키마가 아니라 **정확히 그대로의** 스키마(파일에서 읽어 오거나 데이터베이스에서 생성한 스키마)를 내보내야 할 때.
|
||||
* 결과를 완전히 제어해야 할 때: `_meta`, `is_error`, `structured_content`의 모든 키.
|
||||
* MCP가 정의하지 않은 메서드를 처리해야 할 때.
|
||||
|
||||
그 밖의 모든 경우에는 `MCPServer`를 계속 사용하세요.
|
||||
|
||||
## 같은 도구를 직접 작성하기 {#the-same-tool-by-hand}
|
||||
|
||||
다음은 **[도구](../servers/tools.md)**에서 `@mcp.tool()` 아홉 줄로 작성한 `search_books` 도구에서 문법적 편의를 걷어 낸 모습입니다.
|
||||
|
||||
```python title="server.py" hl_lines="22 26 32"
|
||||
--8<-- "docs_src/lowlevel/tutorial001.py"
|
||||
```
|
||||
|
||||
세 가지가 바뀌었고, 이 세 가지가 저수준 API의 전부입니다.
|
||||
|
||||
* **핸들러는 생성자 매개변수입니다.** `on_list_tools=`와 `on_call_tool=`은 `Server(...)`에 들어갑니다. 여기에는 데코레이터가 없으며, 모든 핸들러의 형태가 `async (ctx, params) -> result`로 동일합니다.
|
||||
* **입력 스키마를 직접 작성합니다.** `Tool.input_schema`는 평범한 JSON Schema `dict`입니다. 타입 힌트에서 대신 도출해 주는 곳이 없습니다. 도출할 타입 힌트 자체가 없기 때문입니다.
|
||||
* **결과를 직접 만듭니다.** `CallToolResult(content=[TextContent(...)])`를 손으로 작성합니다. 감싸거나 변환하거나 반환 어노테이션에서 추론하는 것은 아무것도 없습니다.
|
||||
|
||||
`params`는 파싱된 요청입니다. `CallToolRequestParams`에는 `.name`과 `.arguments`가 있습니다. `ctx`는 `ServerRequestContext`입니다. 클라이언트에 다시 말을 거는 데 쓰는 `ctx.session`, 그리고 `ctx.lifespan_context`, `ctx.request_id`, 요청에 실려 들어온 `_meta`인 `ctx.meta`가 있습니다.
|
||||
|
||||
!!! info
|
||||
FastAPI를 써 봤다면 이 관계를 이미 알고 있습니다. `MCPServer`는 데코레이터와 타입 힌트로 이루어진 계층이고, `Server`는 그 아래의 Starlette에 해당합니다. 둘은 경쟁 관계가 아닙니다. `MCPServer`는 `Server`를 생성하고 그 위에 바로 이런 핸들러를 등록합니다.
|
||||
|
||||
### 직접 해 보기 {#try-it}
|
||||
|
||||
이번에는 Inspector를 쓸 수 없습니다. `mcp dev`와 `mcp run`은 `MCPServer`만 받습니다. 인메모리 `Client`는 상관하지 않으며, `MCPServer`를 받는 것과 똑같이 저수준 `Server`도 받습니다.
|
||||
|
||||
```python title="main.py"
|
||||
import asyncio
|
||||
|
||||
from mcp import Client
|
||||
|
||||
from server import server
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with Client(server) as client:
|
||||
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
|
||||
print(result.content)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
```text
|
||||
[TextContent(type='text', text="Found 3 books matching 'dune' (showing up to 5).", annotations=None, meta=None)]
|
||||
```
|
||||
|
||||
`@mcp.tool()` 버전이 만들어 낸 것과 같은 텍스트입니다. 숨김없이 말하면 차이점이 두 가지 있습니다.
|
||||
|
||||
* `result.structured_content`가 `None`입니다. 고수준 서버는 `-> str` 반환값을 `{"result": ...}`로 감싸 주지만, 여기서는 직접 만들지 않은 것을 대신 만들어 주는 곳이 없습니다.
|
||||
* `list_tools`는 **직접** 입력한 스키마를 글자 하나까지 그대로 반환합니다. 고수준 버전에는 모든 속성에 `"title": "Query"`가, 루트에 `"title": "search_booksArguments"`가 있었습니다. Pydantic이 남긴 흔적입니다. 여기서는 와이어에 실린 것이라면 전부 직접 넣은 것입니다.
|
||||
|
||||
## 자동 검증 없음 {#nothing-is-checked-for-you}
|
||||
|
||||
`MCPServer`는 함수가 실행되기도 전에, 생성한 스키마에 호출을 대조해 검증하여 잘못된 인수를 거부합니다(**[도구](../servers/tools.md)**).
|
||||
|
||||
`Server`는 그렇게 하지 않습니다. `input_schema`는 클라이언트에 **알려지기만** 할 뿐, `params.arguments`에 **적용되는** 일은 없습니다.
|
||||
|
||||
!!! check
|
||||
`limit` 없이 `search_books`를 호출하면 `args["limit"]`에서 `KeyError`가 발생합니다. 클라이언트가 보는 것은 다음과 같습니다.
|
||||
|
||||
```text
|
||||
MCPError: Internal server error
|
||||
```
|
||||
|
||||
코드 `-32603`의 JSON-RPC 오류이며, 메시지는 일부러 포괄적으로 되어 있습니다. SDK는 트레이스백을 원격 호출자에게 새어 나가게 하지 않습니다. 모델은 무엇을 잘못했는지 끝내 알지 못하므로 재시도할 수 없습니다. (테스트에서는 `raise_exceptions=True`로 실제 예외를 대신 드러낼 수 있습니다. **[테스트](../get-started/testing.md)**를 참고하세요.)
|
||||
|
||||
이것은 일반적인 규칙입니다. 저수준 핸들러에서 발생한 예외는 **언제나** 프로토콜 오류이며, 결코 `is_error=True` 도구 결과가 되지 않습니다. 모델이 실패 내용을 읽고 복구하기를 원한다면 `params.arguments`를 직접 검증하고 `CallToolResult(content=[TextContent(...)], is_error=True)`를 반환하세요. 이 두 가지 실패는 **[오류 처리](../servers/handling-errors.md)**에서 다룹니다.
|
||||
|
||||
## 도구 두 개, 핸들러 하나 {#two-tools-one-handler}
|
||||
|
||||
`on_call_tool`은 서버에 있는 모든 도구의 단일 진입점입니다. `params.name`으로 분기합니다.
|
||||
|
||||
```python title="server.py" hl_lines="38-43"
|
||||
--8<-- "docs_src/lowlevel/tutorial002.py"
|
||||
```
|
||||
|
||||
* `list_tools`는 둘 다 알립니다. `call_tool`은 이름에 따라 디스패치합니다.
|
||||
* `else` 분기가 중요합니다. `Server`는 목록에 올린 적 없는 이름의 `tools/call`도 기꺼이 핸들러로 그대로 전달합니다. 거기서 예외를 일으키면 위와 같은 `-32603`이 됩니다.
|
||||
|
||||
## 구조화된 출력 직접 작성하기 {#structured-output-by-hand}
|
||||
|
||||
`Tool`에 `output_schema`를 선언하고 결과에 `structured_content`를 넣습니다. 둘 다 직접 작성합니다.
|
||||
|
||||
```python title="server.py" hl_lines="19-23 36"
|
||||
--8<-- "docs_src/lowlevel/tutorial003.py"
|
||||
```
|
||||
|
||||
호출하면 결과에 두 가지 표현이 모두 실립니다.
|
||||
|
||||
```json
|
||||
{
|
||||
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
|
||||
"structuredContent": {"matches": 3, "query": "dune"},
|
||||
"isError": false,
|
||||
"resultType": "complete",
|
||||
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
|
||||
}
|
||||
```
|
||||
|
||||
`_meta` 블록은 서버의 신원 도장입니다. SDK는 2026 계열 프로토콜의 모든 결과에 이것을 추가하며, `version`은 생성자에서 가져옵니다(버전을 설정하지 않은 서버는 빈 문자열을 보고합니다). 자신을 드러내서는 안 되는 서버는 미들웨어로 이 키를 제거할 수 있습니다. 미들웨어는 자신이 반환하는 결과를 소유하기 때문입니다.
|
||||
|
||||
서버는 두 필드를 비교하지 않습니다. 이 SDK의 `Client`는 비교합니다. 선언한 `output_schema`를 만족하지 않는 `structured_content`를 반환하면 `call_tool`이 `RuntimeError`를 일으키는데, 메시지는 `Invalid structured content returned by tool search_books`로 시작해 `jsonschema` 실패 내용을 인용합니다. 스키마를 약속하기는 쉽지만, 지키는 것은 작성자의 몫입니다. 반환 타입과 스키마의 전체 단계는 **[구조화된 출력](../servers/structured-output.md)**에서 확인하세요.
|
||||
|
||||
## `_meta`: 모델이 아닌 애플리케이션을 위한 데이터 {#\_meta-for-the-application-not-the-model}
|
||||
|
||||
`content`는 답변 중 모델이 읽는 부분입니다. `structured_content`는 같은 답변을 타입이 있는 데이터로 나타낸 것입니다. `_meta`는 세 번째 채널입니다. 답변의 일부가 전혀 아니면서 결과에 함께 실려 **클라이언트 애플리케이션**으로 가는 데이터입니다.
|
||||
|
||||
레코드 ID, 트레이스 ID처럼 UI에는 필요하고 프롬프트에는 필요 없는 것이라면 무엇이든 여기에 넣으세요.
|
||||
|
||||
```python title="server.py" hl_lines="37"
|
||||
--8<-- "docs_src/lowlevel/tutorial004.py"
|
||||
```
|
||||
|
||||
* 구성할 때는 와이어 이름인 `_meta=`로 씁니다. 클라이언트는 `result.meta`로 읽습니다.
|
||||
* 키에 네임스페이스를 붙이세요(`bookshop/record_ids`). `io.modelcontextprotocol/*` 키는 프로토콜이 예약해 두었습니다.
|
||||
|
||||
!!! warning
|
||||
`_meta`는 작성자와 클라이언트 애플리케이션 사이의 관례이지, 무엇이 모델에 도달하는지에 관한 보장이
|
||||
아닙니다. 무엇을 렌더링할지는 호스트가 결정합니다. 도구 결과의 어느 부분에도 절대 비밀 값을 넣지 마세요.
|
||||
|
||||
## 핸들러에 따라 결정되는 기능 {#capabilities-follow-your-handlers}
|
||||
|
||||
`Server`는 핸들러를 제공한 메서드 군만 정확히 알립니다. 위의 `Bookshop`은 `on_list_tools`와 `on_call_tool`만 전달하고 다른 것은 전달하지 않으므로, 여기에 연결하는 클라이언트가 보는 것은 다음과 같습니다.
|
||||
|
||||
```json
|
||||
{"tools": {"listChanged": false}}
|
||||
```
|
||||
|
||||
`resources`도 `prompts`도 없습니다. 뒷받침할 것이 없기 때문입니다. `on_list_prompts`를 전달하면 `prompts`가 나타나고, `on_completion`을 전달하면 `completions`가 나타납니다.
|
||||
|
||||
`MCPServer`는 등록한 것이 있든 없든 항상 도구, 리소스, 프롬프트를 알립니다. 관리자 객체가 항상 존재하기 때문입니다. 여기서는 선언이 **곧** 생성자 호출입니다.
|
||||
|
||||
## lifespan 제네릭 {#the-lifespan-generic}
|
||||
|
||||
`Server`는 lifespan이 yield하는 타입에 대해 제네릭입니다. 어노테이션을 한 번 달면 그 객체가 나타나는 모든 곳에서 타입이 지정됩니다.
|
||||
|
||||
```python title="server.py" hl_lines="24-26 44-45 50"
|
||||
--8<-- "docs_src/lowlevel/tutorial005.py"
|
||||
```
|
||||
|
||||
* lifespan은 `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]` 형태이며, `async` 제너레이터에 `@asynccontextmanager`를 붙이면 정확히 이것이 됩니다.
|
||||
* `yield`한 것은 무엇이든 `ctx.lifespan_context`가 되고, 핸들러에 `ServerRequestContext[Catalog]` 어노테이션이 달려 있으므로 `.search(...)`가 자동 완성되고 타입 검사를 통과합니다.
|
||||
* 서버가 시작할 때 한 번 진입하고 멈출 때 한 번 빠져나옵니다. 시작, 정리, 그리고 같은 개념의 `MCPServer` 버전은 **[Lifespan](../handlers/lifespan.md)**에서 확인하세요.
|
||||
|
||||
`lifespan=` 인수가 없으면 `ctx.lifespan_context`는 빈 `dict`입니다.
|
||||
|
||||
## 직접 정의하는 메서드 {#a-method-of-your-own}
|
||||
|
||||
생성자는 MCP가 정의한 메서드를 다룹니다. 그 밖의 모든 것은 `add_request_handler`가 다룹니다.
|
||||
|
||||
```python title="server.py" hl_lines="35-36 39-40 43-44 48"
|
||||
--8<-- "docs_src/lowlevel/tutorial006.py"
|
||||
```
|
||||
|
||||
* 첫 번째 인수는 메서드 문자열입니다. 알림에는 짝이 되는 `add_notification_handler`가 있습니다.
|
||||
* `params_type`은 핸들러가 실행되기 **전에** 들어오는 `params`를 검증하는 기준 모델입니다. 따라서 커스텀 메서드는 도구가 받지 못하는 검증을 **받습니다**. `_meta` 필드가 다른 모든 메서드처럼 파싱되도록 `RequestParams`를 상속하세요.
|
||||
* 핸들러는 `BaseModel`, `dict`, `None` 중 하나를 반환합니다. SDK가 이를 JSON-RPC 결과로 직렬화합니다.
|
||||
|
||||
솔직한 단서 하나가 있습니다. 고수준 `Client`에는 MCP가 정의한 메서드용 동사만 있으므로 `client.reindex()`는 없습니다. 벤더 메서드는 그 메서드의 존재를 이미 아는 상대를 위한 것입니다. 함께 배포하는 클라이언트나, JSON-RPC를 말하는 자체 서비스가 여기에 해당합니다.
|
||||
|
||||
차지할 수 없는 메서드가 하나 있습니다.
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
핸드셰이크는 러너의 소유입니다. `server/discover`, `ping`, 그 밖의 모든 내장 메서드는 자유롭게 대체할 수 있습니다.
|
||||
|
||||
!!! tip
|
||||
오류 메시지에 언급된 `Server.middleware`는 `initialize`를 포함해 들어오는 **모든** 메시지를 감쌉니다. 새 메서드에 응답하는 것이 아니라 트래픽을 관찰하거나 다시 쓰고 싶다면 **[미들웨어](middleware.md)**부터 시작하세요.
|
||||
|
||||
## 나머지 핸들러 {#the-other-handlers}
|
||||
|
||||
다음은 각각 이제 이해할 어휘를 갖춘 개념 하나씩이며, 각각 별도의 페이지가 있습니다.
|
||||
|
||||
* `on_call_tool`, `on_get_prompt`, `on_read_resource`는 호출을 일시 중지하고 클라이언트에 입력을 요청하기 위해 평소의 결과 대신 `InputRequiredResult`를 반환할 수 있습니다. **[다중 왕복 요청](../handlers/multi-round-trip.md)**을 참고하세요. 이 계층답게 대신 설치해 주는 것은 없습니다. `MCPServer`가 기본적으로 `requestState`를 봉인하는 반면, 여기서는 설정한 `request_state`가 `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`로 명시적으로 켜기 전까지 쓴 그대로 와이어를 건너갑니다. 이 한 줄(두 이름 모두 `mcp.server.request_state`에서 임포트합니다)이면 `MCPServer`가 수행하는 것과 동일한 봉인과 검증이 이루어집니다(**[`requestState` 보호하기](../handlers/multi-round-trip.md#protecting-requeststate)**).
|
||||
* `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion`은 나머지 프리미티브용으로 같은 `(ctx, params) -> result` 형태입니다.
|
||||
* `on_subscriptions_listen`은 2026-07-28의 `subscriptions/listen` 스트림을 제공합니다. `SubscriptionBus` 위에 만든 `ListenHandler`를 전달하고 다른 핸들러에서 버스로 이벤트를 발행하세요. 전체 구성은 **[구독](../handlers/subscriptions.md)**에서 확인하세요.
|
||||
* `server.streamable_http_app()`은 `MCPServer`의 것과 같은 Starlette 앱을 반환합니다. **[서버 실행하기](../run/index.md)**에서 다른 ASGI 앱을 배포하는 방식 그대로 배포하세요. 여기에는 `server.run(transport=...)` 같은 것이 없습니다. `server.run(read_stream, write_stream, server.create_initialization_options())` 호출이 스트림 한 쌍 위에서 연결 하나를 구동하며, 이 한 줄이 전부입니다.
|
||||
|
||||
## 요약 {#recap}
|
||||
|
||||
* 저수준 `Server`는 핸들러를 `on_*` **생성자 매개변수**로 받으며, 모든 핸들러는 `async (ctx, params) -> result`입니다.
|
||||
* `input_schema` dict를 직접 작성하고 `CallToolResult`를 직접 만듭니다. 대신 도출하거나 감싸거나 검증해 주는 것은 없습니다.
|
||||
* 핸들러의 예외는 `-32603` 프로토콜 오류입니다. 모델이 읽을 수 있는 도구 오류는 `is_error=True`인 `CallToolResult`이며 **직접** 반환해야 합니다.
|
||||
* 결과의 `_meta`는 모델이 아니라 클라이언트 애플리케이션에 보내는 것입니다.
|
||||
* `Server[T]`는 lifespan이 yield하는 것에 대해 제네릭이며, `ctx.lifespan_context`는 타입이 지정된 `T`입니다.
|
||||
* `add_request_handler(method, params_type, handler)`는 어떤 메서드든 제공합니다. `initialize`는 예약되어 있습니다.
|
||||
* `Server`가 알리는 기능은 등록한 핸들러에서 도출됩니다.
|
||||
|
||||
`Client(server)`가 두 서버를 똑같이 다룬 것은 둘이 **같은** 프로토콜이기 때문이며, 바로 그 점이 핵심입니다. 그다음 아래 계층은 클래스가 아닙니다. 바로 **[미들웨어](middleware.md)**입니다.
|
||||
125
i18n/ko/pages/advanced/middleware.md
Normal file
125
i18n/ko/pages/advanced/middleware.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
---
|
||||
translation:
|
||||
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
|
||||
tool: 1
|
||||
---
|
||||
# 미들웨어 {#middleware}
|
||||
|
||||
**미들웨어**는 서버가 받는 모든 메시지를 감싸는 하나의 async 함수입니다.
|
||||
|
||||
`async (ctx, call_next)` 형태로 작성해서 `server.middleware`에 추가하면 됩니다. 이것이 API의 전부입니다.
|
||||
|
||||
!!! warning
|
||||
미들웨어 목록은 소스에서 **잠정적**(provisional)으로 표시되어 있습니다. 시그니처와 동작 의미는
|
||||
2.x 마이너 릴리스에서 바뀔 수 있습니다. 메시지를 **관찰**(시간 측정, 로깅, 트레이싱)하고
|
||||
**거부**하는 데 사용하세요. 서버가 딛고 서는 기반으로 삼지는 마세요.
|
||||
|
||||
`MCPServer`는 생성 시 목록을 받아(`MCPServer(name, middleware=[...])`) `mcp.middleware`로 노출하고,
|
||||
저수준 `Server`는 같은 목록을 `server.middleware`로 노출합니다. 아래 예제는 저수준 `Server`를
|
||||
사용합니다. `Server(name, on_call_tool=...)`가 처음이라면
|
||||
**[저수준 Server](low-level-server.md)**를 먼저 읽으세요.
|
||||
|
||||
## 시간을 재는 미들웨어 {#a-timing-middleware}
|
||||
|
||||
서버 하나, 도구 하나, 그리고 메시지마다 걸린 시간을 로그로 남기는 미들웨어 하나입니다.
|
||||
|
||||
```python title="server.py" hl_lines="39-45 49"
|
||||
--8<-- "docs_src/middleware/tutorial001.py"
|
||||
```
|
||||
|
||||
* `ctx`는 핸들러가 받는 것과 같은 `ServerRequestContext`입니다. `ctx.method`는 원시 메서드
|
||||
문자열이고, `ctx.params`는 어떤 검증도 거치기 **전**의 원시 params입니다.
|
||||
* `call_next(ctx)`는 체인의 나머지, 즉 검증, 핸들러 조회, 작성한 핸들러를 실행합니다.
|
||||
반환된 값을 그대로 반환하면 응답은 손대지 않은 채로 나갑니다.
|
||||
* `try`/`finally`는 의도적인 선택입니다. 예외를 일으키는 핸들러도 시간이 측정됩니다. 실패는
|
||||
`call_next`에서 빠져나오는 예외로 미들웨어에 도달하기 때문입니다.
|
||||
* `server.middleware.append(...)`로 등록합니다. 목록은 바깥쪽부터 실행되므로
|
||||
`middleware[0]`이 와이어에 가장 가까운 미들웨어입니다.
|
||||
|
||||
### 직접 해 보기 {#try-it}
|
||||
|
||||
클라이언트를 연결하고, 도구 목록을 조회하고, 하나를 호출해 보세요. 로그에는 **세** 줄이 남습니다.
|
||||
|
||||
```text
|
||||
server/discover took 18.3 ms
|
||||
tools/list took 0.1 ms
|
||||
tools/call took 0.1 ms
|
||||
```
|
||||
|
||||
호출은 두 번 했는데 줄은 세 개입니다. 첫 번째 줄은 `server/discover`로, 아무것도 요청하기 전에
|
||||
클라이언트가 연결을 설정하려고 보낸 요청입니다.
|
||||
|
||||
바로 이것이 핵심입니다. 미들웨어는 들어오는 **모든** 메시지를 감쌉니다.
|
||||
|
||||
* 연결 설정. `server/discover`이거나, 레거시 세션에서는 `initialize`와
|
||||
`notifications/initialized`입니다.
|
||||
* 모든 요청과 모든 알림. 알림의 경우 `ctx.request_id is None`이고,
|
||||
`call_next(ctx)`는 `None`을 반환하며, 무엇을 반환하든 버려집니다.
|
||||
* 서버에 핸들러가 없는 메서드까지도 포함됩니다. `call_next`가
|
||||
`MCPError(-32601, "Method not found")`를 일으키고, 이 예외는 클라이언트로 가는 길에
|
||||
미들웨어를 **통과합니다**.
|
||||
|
||||
## 미들웨어 안에서 할 수 있는 일 {#what-you-can-do-inside-one}
|
||||
|
||||
망설임이 적게 필요한 것부터 순서대로 나열합니다.
|
||||
|
||||
* **관찰.** 시간을 재고, 횟수를 세고, 로그를 남기세요. 위의 예제가 이에 해당합니다.
|
||||
* **거부.** `call_next(ctx)`를 호출하는 **대신** `MCPError`를 일으키면 그 메시지 하나에
|
||||
JSON-RPC 오류로 응답합니다. 연결은 유지되고 다음 메시지는 그대로 통과합니다. 서버가
|
||||
호출자별로 `subscriptions/listen`을 제한하는 방법이 바로 이것입니다. 구독 페이지의
|
||||
**[누가 지켜볼 수 있는지 정하기](../handlers/subscriptions.md#deciding-who-may-watch)**에서
|
||||
단계별로 설명합니다.
|
||||
* **재작성.** `ctx`는 데이터클래스입니다. `await call_next(dataclasses.replace(ctx, params=...))`는
|
||||
체인의 나머지에 클라이언트가 보낸 것과 다른 params를 넘깁니다. `initialize`에는 절대 이렇게
|
||||
하지 마세요. 클라이언트가 돌려받는 결과는 재작성한 params로 만들어지지만, 서버는 원래 와이어
|
||||
params를 기준으로 연결 상태를 확정합니다. 양쪽이 무엇을 협상했는지 서로 다르게 이해한 채로
|
||||
핸드셰이크를 마칠 수 있습니다.
|
||||
* **응답.** `call_next(ctx)`를 호출하지 않고 결과를 반환하면 그 결과가 응답으로 클라이언트에
|
||||
전달됩니다. `call_next`는 완성된 와이어 형식을 넘겨주고, 파이프라인은 반환한 값을 손보지
|
||||
않으므로 봉투 전체가 미들웨어의 몫입니다. 2026년 세대의 연결에서는 `serverInfo` `_meta`
|
||||
스탬프가 여기에 포함되는데, SDK는 이를 핸들러 결과에는 추가하지만 미들웨어가 반환한 결과에는
|
||||
추가하지 않습니다.
|
||||
|
||||
!!! check
|
||||
`initialize`도 미들웨어가 감싸는 대상 중 하나이며, 미들웨어는 이에 대해 얻을 수
|
||||
있는 **유일한** 훅입니다. `add_request_handler`로 가로채려고 하면 SDK가 거부합니다.
|
||||
|
||||
```text
|
||||
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
|
||||
use Server.middleware to observe or wrap initialization
|
||||
```
|
||||
|
||||
!!! warning
|
||||
`initialize`는 인라인으로 처리됩니다. 미들웨어 체인이 반환할 때까지 서버는 들어오는 메시지를
|
||||
더 읽지 않습니다. 따라서 `initialize`를 처리하는 동안 서버에서 클라이언트로 가는 요청
|
||||
(`ctx.session.send_request(...)`, 엘리시테이션(elicitation))을 await하면 **연결이 교착 상태에
|
||||
빠집니다**. 기다리는 응답은 결코 읽힐 수 없기 때문입니다. 보내고 잊는 방식의 알림은 괜찮습니다.
|
||||
|
||||
## 기본으로 켜져 있는 단 하나의 미들웨어 {#the-one-middleware-that-ships-on-by-default}
|
||||
|
||||
SDK에는 미들웨어가 정확히 하나 포함되어 있으며, 이미 서버의 목록에 들어 있습니다. 모든 메시지마다
|
||||
OpenTelemetry 스팬을 내보내는 미들웨어입니다. 직접 추가할 필요가 없고, 대부분의 경우 신경 쓸
|
||||
필요도 없습니다. 익스포터를 설치하기 전까지는 아무 일도 하지 않으며, 별도의 페이지가 있습니다.
|
||||
**[OpenTelemetry](../run/opentelemetry.md)**를 참고하세요.
|
||||
|
||||
!!! info
|
||||
ASGI 미들웨어를 작성해 본 적이 있다면 이 형태가 이미 익숙할 것입니다. Starlette의
|
||||
`(scope, receive, send)`가 `(ctx, call_next)`가 되었고, 트랜스포트 **이후에**, 원시 HTTP 요청이
|
||||
아니라 디코딩된 메시지를 대상으로 실행됩니다. 둘은 함께 조합됩니다. `streamable_http_app()`에
|
||||
붙인 Starlette 미들웨어는 HTTP를 보고, 이 미들웨어는 MCP를 봅니다.
|
||||
|
||||
## 요약 {#recap}
|
||||
|
||||
* 미들웨어는 `async (ctx, call_next) -> result` 형태이며, `MCPServer(middleware=[...])`로
|
||||
전달하거나(또는 `mcp.middleware`에 추가하거나) 저수준 `Server`에서는 `server.middleware`에
|
||||
추가합니다.
|
||||
* 들어오는 **모든** 메시지(`server/discover`, `initialize`, 요청, 알림, 알 수 없는 메서드)를
|
||||
감싸며 바깥쪽부터 실행됩니다.
|
||||
* `ctx.request_id is None`으로 알림과 요청을 구분합니다.
|
||||
* `call_next`를 호출하는 대신 예외를 일으키면 메시지 하나를 거부합니다. 연결은 유지됩니다.
|
||||
* SDK 자체의 OpenTelemetry 트레이싱도 미들웨어이며, 이미 목록에 있습니다.
|
||||
**[OpenTelemetry](../run/opentelemetry.md)**를 참고하세요.
|
||||
* 이 표면 전체가 잠정적입니다. 관찰하는 데 사용하고, 그 위에 무언가를 쌓지는 마세요.
|
||||
|
||||
요청을 감싸는 것은 이것이 전부입니다. 요청이 애초에 실행될 수 있는지를 결정하는 것은
|
||||
**[인가](../run/authorization.md)**입니다.
|
||||
85
i18n/ko/pages/advanced/pagination.md
Normal file
85
i18n/ko/pages/advanced/pagination.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
translation:
|
||||
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
|
||||
tool: 1
|
||||
---
|
||||
# 페이지네이션 {#pagination}
|
||||
|
||||
대부분의 서버에는 필요 없는 기능입니다.
|
||||
|
||||
`MCPServer`는 모든 `list_*` 요청에 가진 것을 전부 한 페이지에 담아 `next_cursor=None`으로 응답합니다. 도구, 리소스, 프롬프트가 수십 개 정도라면 이것이 올바른 동작이며, 설정할 것은 아무것도 없습니다.
|
||||
|
||||
페이지네이션은 리소스 목록이 사실상 데이터베이스인 서버를 위한 것입니다. 한 번의 응답으로 직렬화하기에는 무리인 수천 개의 행이 있는 경우입니다. 프로토콜의 해법은 **커서**입니다. 서버는 페이지 하나와 불투명한 토큰을 함께 반환하고, 클라이언트는 그 토큰을 다시 보내 다음 페이지를 받습니다.
|
||||
|
||||
`@mcp.resource()`에는 이를 위한 훅이 없습니다. 페이지를 나누려면 **[저수준 Server](low-level-server.md)**에서 목록 핸들러를 직접 작성해야 합니다.
|
||||
|
||||
## 페이지를 나누는 서버 {#a-server-that-pages}
|
||||
|
||||
```python title="server.py" hl_lines="12 15-16"
|
||||
--8<-- "docs_src/pagination/tutorial001.py"
|
||||
```
|
||||
|
||||
* 저수준 `Server`에서 핸들러는 데코레이터가 아니라 생성자 인자입니다. `on_list_resources`가 모든 `resources/list` 요청에 응답하며, 연결 작업은 이것이 전부입니다.
|
||||
* 페이지를 나누는 핸들러는 모두 `params: PaginatedRequestParams | None` 타입을 받으며, 예제는 두 경우를 모두 처리합니다. 다만 실제 연결에서는 SDK가 `None`을 넘기는 일이 없습니다(`params` 멤버가 없는 요청은 기본값이 채워진 모델로 핸들러에 도달합니다). 따라서 의미 있는 신호는 `params.cursor is None`이며, 이는 **처음부터 시작하라**는 뜻입니다.
|
||||
* 커서가 **무엇인지**는 직접 정합니다. 여기서는 문자열로 표현한 오프셋입니다. 타임스탬프, 기본 키, base64 덩어리 등 내보낼 때 만들어 낼 수 있고 돌아왔을 때 알아볼 수 있는 것이면 무엇이든 됩니다.
|
||||
* `next_cursor=None`은 "이것이 마지막 페이지였다"고 알리는 방법입니다. 개수도, 총계도, `has_more`도 없습니다. `None`이 신호의 전부입니다.
|
||||
|
||||
!!! tip
|
||||
`PAGE_SIZE`를 10으로 둔 것은 예제를 읽기 쉽게 하기 위해서입니다. 실제 값은 엔드포인트마다 정하세요.
|
||||
한 줄짜리 리소스 목록이라면 500개 페이지도 감당할 수 있지만, 덩치 큰 프롬프트 템플릿 목록은 그럴 수 없습니다.
|
||||
클라이언트는 여기에 관여할 수 없으며, 이는 의도된 설계입니다.
|
||||
|
||||
### 직접 해 보기 {#try-it}
|
||||
|
||||
`Client(server)`는 `MCPServer`에 연결할 때와 똑같이 저수준 `Server`에 인메모리로 연결합니다.
|
||||
|
||||
인자 없이 `list_resources()`를 호출하세요. `book-1`부터 `book-10`까지 리소스 10개가 돌아오고, `next_cursor`는 문자열 `"10"`입니다.
|
||||
|
||||
이를 `list_resources(cursor="10")`으로 다시 넘기면 첫 번째 리소스는 `book-11`이고, 새 `next_cursor`는 `"20"`입니다.
|
||||
|
||||
열 번째 페이지는 `next_cursor`가 `None`으로 설정되어 돌아옵니다. 끝입니다.
|
||||
|
||||
## 클라이언트 루프 {#the-client-loop}
|
||||
|
||||
`Client`의 모든 `list_*` 메서드(`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`)는 `cursor=` 키워드를 받습니다. 페이지로 나뉜 목록을 전부 가져오는 것은 `while True` 하나면 됩니다.
|
||||
|
||||
```python title="client.py" hl_lines="26-32"
|
||||
--8<-- "docs_src/pagination/tutorial002.py"
|
||||
```
|
||||
|
||||
* `cursor`는 `None`으로 시작하므로 첫 요청에는 커서가 실리지 않습니다.
|
||||
* `next_cursor`를 확인하기 **전에** 결과를 덧붙이세요. 마지막 페이지에도 리소스가 있습니다.
|
||||
* `next_cursor is None`이 종료 조건입니다. 그 외의 값은 손대지 않고 그대로 `cursor=`에 다시 넣습니다.
|
||||
|
||||
이 파일의 `main()`을 실행하면 `100 resources`가 출력됩니다. 열 개씩 열 페이지가, 페이지가 열 개라는 사실조차 모르는 루프에 의해 하나로 이어 붙여진 결과입니다.
|
||||
|
||||
이 루프는 **[클라이언트](../client/index.md)**에서 모든 `list_*` 동사에 대해 보여 주는 것과 같은 루프이며, 페이지를 나누지 않는 서버에 대해서도 비용이 들지 않습니다. 첫 응답에서 `next_cursor`가 `None`이므로 루프는 한 번만 돕니다.
|
||||
|
||||
## 세 가지 규칙 {#the-three-rules}
|
||||
|
||||
**커서는 불투명합니다.** 클라이언트는 커서를 파싱하거나, 만들거나, 추측해서는 안 됩니다. 커서를 얻는 유일하게 정당한 출처는 이전 페이지의 `next_cursor`를 그대로 쓰는 것입니다.
|
||||
|
||||
**페이지 크기는 서버가 정합니다.** 프로토콜에 `limit=`은 없습니다. 다른 페이지 크기가 필요하면 서버를 바꿔야 합니다.
|
||||
|
||||
**페이징을 무시하는 클라이언트도 여전히 동작합니다.** `list_resources()`를 한 번 호출하고, 처음 열 개를 받고, 버린 `next_cursor`는 알아채지 못합니다. 아무것도 깨지지 않습니다. 덜 보일 뿐입니다.
|
||||
|
||||
!!! check
|
||||
불투명하다는 것은 말 그대로 불투명하다는 뜻입니다. 커서를 지어내면(`list_resources(cursor="page-2")`)
|
||||
프로토콜이 해 줄 수 있는 것은 아무것도 없습니다. 이 서버는 `int("page-2")`를 시도하고, 핸들러는 예외를
|
||||
일으키며, 클라이언트에게 돌아오는 것은 다음과 같습니다.
|
||||
|
||||
```text
|
||||
MCPError(-32603, 'Internal server error', None)
|
||||
```
|
||||
|
||||
서버에서 받지 않은 커서는 기능 요청이 아니라 버그입니다.
|
||||
|
||||
## 요약 {#recap}
|
||||
|
||||
* `MCPServer`는 모든 것을 한 페이지로 반환합니다. 페이지네이션은 선택 사항이며, 저수준 `Server`에서 선택합니다.
|
||||
* `on_list_resources`(그리고 `on_list_tools`, `on_list_prompts`, `on_list_resource_templates`)는 `PaginatedRequestParams | None`을 받으며, 첫 페이지에서는 `params.cursor`가 `None`입니다.
|
||||
* 페이지와 함께 `next_cursor`를 반환합니다. 나중에 알아볼 수 있는 문자열이면 무엇이든 되고, 남은 것이 없으면 `None`입니다.
|
||||
* 클라이언트 루프는 `cursor=`를 전달하고, 누적하고, `next_cursor is None`이 될 때까지 반복합니다.
|
||||
* 커서는 불투명하고, 페이지 크기는 서버가 정하며, 페이징을 하지 않는 클라이언트도 첫 페이지는 받습니다.
|
||||
|
||||
직접 작성하는 `Server` API의 나머지(`on_call_tool`, `input_schema` 딕셔너리, `_meta`)는 **[저수준 Server](low-level-server.md)**에서 확인하세요.
|
||||
Loading…
Reference in a new issue