첫 단계

첫 단계 (First steps)

**시작 페이지**는 빠르게 흘러갑니다. 서버를 쓰고, 돌리고, 도구를 호출하죠.

이 페이지는 천천히, 서버가 노출할 수 있는 세 가지 전부를, 그리고 그 과정의 모든 것에 이름을 붙여가며 진행합니다.

호스트, 클라이언트, 서버

이제부터 모든 페이지에서 보게 될 세 단어입니다.

  • 호스트(host) 는 LLM 애플리케이션입니다. Claude, IDE, 에이전트 런타임이죠. 사용자가 대화하고 있는 바로 그 대상입니다.
  • 클라이언트(client) 는 호스트 안에 살면서 MCP를 말합니다. 호스트는 자신이 연결한 서버마다 클라이언트를 하나씩 돌립니다.
  • 서버(server) 는 여러분이 이 SDK로 만드는 것입니다. 서버는 클라이언트에게 뭔가를 노출하지요. 모델과 직접 대화하지는 않습니다.

여러분이 서버를 씁니다. 호스트는 다른 사람의 제품이에요. SDK는 여러분에게 Client라는 클래스도 주는데, 이건 호스트가 서버에 URL로 연결하거나 서브프로세스로 실행할 때 쓰는 것과 같은 클래스입니다. 이 페이지 뒤쪽에 나오고, 여러분이 서버를 테스트할 때도 이걸 씁니다.

세 가지 기본 요소(프리미티브)

서버는 정확히 세 종류의 것을 노출합니다. 무엇이 이들을 구분하느냐, 바로 누가 쓰기로 결정하느냐입니다.

프리미티브 제어 주체 무엇인가 예시
도구(Tools) 모델 모델이 행동하기 위해 호출하는 함수 API 호출, 데이터베이스 쓰기
리소스(Resources) 애플리케이션 호스트가 모델의 컨텍스트로 불러들이는 데이터 파일 내용, API 응답
프롬프트(Prompts) 사용자 사용자가 이름으로 호출하는 재사용 메시지 템플릿 슬래시 명령, 메뉴 항목

"제어 주체"가 바로 이 구분의 핵심입니다. 도구는 모델이 호출하기로 결정했기에 실행됩니다. 리소스는 애플리케이션이 모델에게 필요하다고 결정해서 붙습니다. 프롬프트는 사용자가 골라서 실행되지요.

!!! info 웹 API를 만들어 본 적 있다면 이미 직관의 대부분을 갖고 있습니다. 리소스GET(데이터를 불러오고 아무것도 바꾸지 않음), 도구POST(일을 하고 부수 효과가 있을 수 있음)입니다. 프롬프트는 HTTP에 대응물이 없어요. 사용자가 이름으로 실행하는 저장된 쿼리에 더 가깝습니다.

하나의 서버, 세 가지 전부

from mcp.server import MCPServer

mcp = MCPServer("Demo")


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


@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"


@mcp.prompt()
def summarize(text: str) -> str:
    """Summarize a piece of text in one sentence."""
    return f"Summarize the following text in one sentence:\n\n{text}"

평범한 함수 세 개, 데코레이터 세 개. 각 데코레이터가 등록의 전부입니다.

  • @mcp.tool()add도구로 만듭니다.
  • @mcp.resource("greeting://{name}")greeting리소스 템플릿으로 만듭니다. URI의 {name}이 함수의 파라미터가 되죠.
  • @mcp.prompt()summarize프롬프트로 만듭니다. 반환된 문자열이 사용자 메시지가 됩니다.

그 밖의 모든 것(이름, 설명, 인자 스키마)은 SDK가 함수 자체에서 읽습니다. 함수 이름, docstring, 타입 힌트에서요. 그 어떤 것도 따로 선언하지 않았습니다.

!!! tip SDK의 두 반쪽은 import 경로가 둘입니다. from mcp import Clientfrom mcp.server import MCPServer. from mcp import MCPServer는 존재하지 않아요.

직접 해 보기

MCP Inspector로 실행합니다.

uv run mcp dev server.py

출력된 URL을 엽니다. Inspector에는 프리미티브마다 탭이 하나씩 있어요. 순서대로 훑어보세요.

도구(Tools). 항목 하나: add, *Add two numbers.*라고 설명되어 있죠. 폼에 필수 정수 필드 ab가 있습니다. 채워서 호출하면 결과는 3. Inspector는 그 폼을 a: int, b: int에서 만들었어요. 다른 모든 클라이언트도 그렇게 합니다.

리소스(Resources). Resources 목록은 비어 있습니다. greetingResource Templates 아래 있는데, greeting://{name}에는 파라미터가 있어서 누군가 name을 채우기 전까지는 나열할 단일 리소스가 없기 때문이에요. World를 주고 읽어 봅니다.

Hello, World!

프롬프트(Prompts). 항목 하나: summarize, 필수 text 인자 하나를 가집니다. 텍스트를 넣고 가져오면 role: user인 메시지 하나에 렌더링된 문자열이 콘텐츠로 담겨 옵니다. 프롬프트란 그게 전부예요. 메시지를 만드는 함수입니다.

Inspector는 여러분의 서버를 stdio로 실행했습니다. MCP 서버가 말할 수 있는 전송 중 하나죠. 아직 하나를 고를 필요는 없습니다. 그건 Running your server 페이지에서 다룹니다.

캐퍼빌리티(Capabilities)

Inspector에서 탭 세 개를 봤죠. 세 개인 걸 어떻게 알았을까요?

클라이언트가 연결하면 서버는 자신의 캐퍼빌리티를 선언합니다. 어떤 요청 계열에 응답할지를요. 클라이언트는 그 선언을 보고 무엇을 요청할지 결정합니다. 여러분은 이걸 쓴 적이 없어요. MCPServer가 대신 선언해 줍니다.

직접 확인해 보세요. 한 터미널에서 server.py를 HTTP로 띄워 두고,

uv run mcp run server.py --transport streamable-http

다른 터미널에서 클라이언트를 그쪽에 붙입니다.

import anyio

from mcp import Client


async def main() -> None:
    async with Client("http://localhost:8000/mcp") as client:
        print(client.server_capabilities.model_dump(exclude_none=True))


if __name__ == "__main__":
    anyio.run(main)
python client.py
{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}

그 딕셔너리가 여러분 서버가 선언한 캐퍼빌리티입니다. 연결하는 모든 클라이언트가 가장 먼저 알게 되는 것이죠.

캐퍼빌리티 클라이언트가 이제 호출할 수 있는 것
tools tools/list, tools/call
resources resources/list, resources/templates/list, resources/read
prompts prompts/list, prompts/get

MCPServer는 세 프리미티브 전부를 서빙하므로 세 가지를 항상 선언합니다.

없는 것도 눈여겨보세요. completions(리소스 템플릿·프롬프트 인자 자동완성)는 여러분이 써야 하는 핸들러가 필요한데, 이 서버엔 그게 없으므로 캐퍼빌리티도 없고, 예의 바른 클라이언트는 요청하지 않습니다. 선택적인 모든 것의 규칙이죠. 등록하면 캐퍼빌리티가 나타납니다. **Completions**가 그걸 증명해요.

!!! info 그 client.py는 완전한 MCP 클라이언트이고, **The Client**가 그 페이지입니다. 테스트에서는 터미널과 포트를 건너뛰고 서버 객체 자체를 넘깁니다. Client(mcp)처럼요. 그것도 한 페이지를 통째로 차지합니다. Testing.

여러분이 쓰지 않은 것

이 페이지를 다시 훑어보세요. 여러분은 작은 파이썬 함수 세 개를 썼습니다. 이런 것들은 쓰지 않았죠.

  • JSON Schema. a: int, b: int 그 자체가 add의 스키마입니다.
  • 요청 핸들러. tools/list, resources/read, prompts/get: 전부 여러분 대신 서빙됩니다.
  • 캐퍼빌리티 선언. MCPServer가 만들어 줬어요.
  • 프로토콜 한 줄. 버전 협상, JSON-RPC 프레이밍, 캐퍼빌리티 교환 — 그 모든 게 mcp devclient.py 안에서 일어났고 여러분은 본 적이 없습니다.

그 비율이 바로 SDK의 핵심입니다.

정리(Recap)

  • 호스트는 LLM 앱, 클라이언트는 그 MCP를 말하는 절반, 서버는 여러분이 만드는 것입니다.
  • 도구는 모델이 제어하고, 리소스는 애플리케이션이, 프롬프트는 사용자가 제어합니다.
  • 프리미티브마다 데코레이터 하나: @mcp.tool(), @mcp.resource(uri), @mcp.prompt(). 이름·설명·스키마는 함수에서 나옵니다.
  • {param}이 있는 URI는 리소스 템플릿이 되어, 구체 리소스와는 별도로 나열됩니다.
  • 서버의 캐퍼빌리티는 여러분 대신 선언되고, 클라이언트는 서버가 선언한 것만 요청합니다.
  • Client("http://localhost:8000/mcp")는 실행 중인 서버와 대화합니다. 대신 서버 객체를 넘기면(Client(mcp)), 첫날부터 테스트 하네스가 됩니다.

다음은 **Connect to a real host**입니다. 이 서버를 진짜로 Claude Desktop이나 IDE 안에 넣는 거죠. 그다음 Testing — 페이지 하나, 인메모리 클라이언트 하나로 동작 여부를 절대 추측하지 않게 됩니다. 그 뒤엔 프리미티브마다 페이지가 하나씩 있는데, 모델이 구동하는 그것부터 시작합니다. Tools.

출처: Python SDK — First steps

더 알아보기 (Learn more)