테스트

테스트 (Testing)

SDK의 Client 클래스 — URL에 연결하거나 서브프로세스를 띄우는 그 클래스 — 는 인메모리로도 연결합니다. 서버 객체를 넘기면 직접 대화하지요.

서브프로세스도, 포트도, 와이어 위의 아무것도 없습니다. FastAPI의 TestClient와 같은 발상입니다.

기본 사용법

하나의 도구만 가진 간단한 서버가 있다고 가정해 볼게요.

from mcp.server import MCPServer

mcp = MCPServer("Calculator")


@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

아래 테스트를 돌리려면 (개발용) 의존성 두 개가 더 필요합니다.

=== "uv"

```bash
uv add --dev pytest inline-snapshot
```

=== "pip"

```bash
pip install pytest inline-snapshot
```

!!! info 이 문서들은 여러분이 pytest를 이미 안다고 가정합니다.

[`inline-snapshot`](https://15r10nk.github.io/inline-snapshot/latest/)은 아래 테스트가 결과 객체 전체를 한 줄로 단언(assert)하는 데 씁니다. 테스트의 출력을 여러분이 보는 `snapshot(...)` 리터럴로 기록하지요. 쓰기 싫다면 import를 빼고 관심 있는 필드만 단언하면 됩니다(`result.content[0].text == "3"`). 다른 테스트처럼요.

이제 테스트입니다.

import pytest
from inline_snapshot import snapshot
from mcp import Client
from mcp.types import CallToolResult, TextContent

from server import mcp


@pytest.fixture
def anyio_backend():  # (1)!
    return "asyncio"


@pytest.fixture
async def client():  # (2)!
    async with Client(mcp, raise_exceptions=True) as c:
        yield c


@pytest.mark.anyio
async def test_call_add_tool(client: Client):
    result = await client.call_tool("add", {"a": 1, "b": 2})
    # Drop the server identity stamp in `_meta`; it is not what this test is about.
    result.meta = None
    assert result == snapshot(
        CallToolResult(
            content=[TextContent(type="text", text="3")],
            structured_content={"result": 3},
        )
    )
  1. trio를 쓴다면 "trio"를 반환하세요. 자세한 내용은 anyio 문서를 보세요.
  2. 이 fixture는 연결된 클라이언트를 yield합니다. client를 받는 모든 테스트는 같은 서버에 대한 새로운 인메모리 연결을 얻습니다.

됐습니다! 이제 테스트를 확장해 더 많은 시나리오를 덮으면 됩니다.

raise_exceptions=True인가요?

잘못될 수 있는 일은 두 종류인데, 이 플래그는 그중 하나만 건드립니다.

여러분의 도구 안에서 일어난 예외는 프로토콜 실패가 아닙니다. is_error=True인 평범한 결과가 되지요(ToolError였다면 모델이 여러분의 메시지를 읽습니다). raise_exceptions는 그걸 바꾸지 않아요. 있든 없든 call_tool은 같은 is_error=True 결과를 반환합니다. 이건 페이지 하나를 통째로 씁니다. Handling errors.

도구 본문 바깥의 실패는 다릅니다. Client(mcp)가 주는 연결에서는 서버가 클라이언트가 보기 전에 그걸 일반적인 "Internal server error"로 살균(sanitise)합니다. 예상치 못한 크래시의 세부 내용을 원격 호출자에게 새서는 안 되니까요. 테스트에서는 정확히 그게 원하지 않는 것이고, raise_exceptions=True가 그걸 바꿉니다. 살균된 메시지 대신 진짜 메시지를 보게 됩니다.

테스트에서는 켜 두세요. 운영 코드에선 의미가 없습니다.

기본적으로 시대 중립(era-neutral)

!!! note Client(mcp)는 프로세스 내에서 연결하고 기본적으로 시대 중립입니다. 서버를 탐색해서 적절한 프로토콜 경로를 고르죠. 레거시 전용 의미론(샘플링·elicitation push, message_handler)을 테스트한다면 mode="legacy"로 고정하세요. 그리고 거기서 raise_exceptions=True는 빼세요 — 레거시 연결은 애초에 살균하지 않고, 그 플래그는 테스트에서가 아니라 서버 태스크 안에서 실패를 다시 던지기 때문입니다.

그 한 줄이 이 문서들이 예제가 동작한다고 장담할 수 있는 이유이기도 합니다. 모든 예제 파일은 SDK 고유의 테스트 스위트가 검증하고, 거의 전부가 정확히 이 클라이언트를 거칩니다. 여러분은 SDK가 자기 자신에게 쓰는 것과 같은 도구를 쓰고 있는 거예요.

동작하고 검증된 서버가 준비됐습니다. 진짜 애플리케이션(Claude Desktop, IDE) 안에 넣는 것은 **Connect to a real host**이고, 그 밖의 모든 서빙 방식은 **Running your server**입니다.

출처: Python SDK — Testing

더 알아보기 (Learn more)