포스트

MCP 서버를 만들었다고 착각하기 쉬운 이유: Host, Client, Server와 도구 호출 흐름

MCP는 한 번의 prompt로 답변 품질을 높이는 기술이 아니라, AI application이 외부 data와 tool을 발견하고 호출하는 방식을 맞추는 protocol입니다. 연결 규격이 생겨도 올바른 server 구현, model의 tool 선택, 사용자 권한과 결과 검증은 여전히 application이 책임져야 합니다.

MCP 구성 개요

MCP는 RAG나 좋은 prompt를 대신하지 않는다

Model Context Protocol이 해결하려는 문제는 model마다 file system, database, API, developer tool 연결을 다시 만드는 통합 비용입니다. 같은 규격으로 capability를 노출하면 host가 server의 기능을 발견하고 model에게 제공할 수 있습니다.

MCP server가 제공하는 대상은 세 종류로 정리됩니다.

구성역할예시
Resources읽을 context와 datafile, API response
Tools실행할 수 있는 functionquery, write, external action
Prompts특정 작업의 template반복 작업 지시 형식

resource를 연결한다고 답이 자동으로 정확해지는 것은 아닙니다. 어떤 data를 읽을지, 검색 결과가 질문과 맞는지, model이 출처를 제대로 사용했는지는 별도 문제입니다. RAG는 필요한 정보를 검색해 context를 구성하는 방법이고, MCP는 그 검색기나 data source를 연결하는 interface로 사용할 수 있습니다.

또한 MCP가 model의 학습 시점 이후 지식을 스스로 갱신하는 것은 아닙니다. server가 현재 data를 반환하고 host가 그 결과를 model에게 전달할 때에만 최신 정보를 사용할 수 있습니다.

Host, Client, Server는 어디서 나뉘나

MCP host-client-server 흐름

세 구성 요소의 책임은 다음과 같습니다.

  • Host: 사용자와 model을 포함하는 application입니다. 어떤 server를 연결하고 어떤 결과를 model에 보낼지 결정합니다.
  • Client: host 안에서 특정 MCP server와 session을 만들고 request, response를 전달합니다.
  • Server: resource, tool, prompt를 실제로 구현하고 공개합니다.

도구 호출 흐름을 줄이면 다음과 같습니다.

1
2
3
4
5
6
7
사용자 요청
→ host가 server의 tool 목록 조회
→ model에 tool schema 제공
→ model이 tool name과 arguments 선택
→ client가 server tool 호출
→ server 결과를 model에 전달
→ model이 최종 응답 또는 다음 tool 호출 생성

여기서 model이 database에 직접 접속하는 것이 아닙니다. model은 tool call을 제안하고, client가 protocol을 통해 server를 호출합니다. 실제 credential과 network access는 server 또는 host의 실행 환경에 있습니다.

이 구분은 오류 추적에도 유용합니다. tool 목록이 비면 client-server 연결을 보고, 잘못된 arguments가 나오면 tool schema와 model 판단을 보고, 올바른 호출인데 결과가 틀리면 server 구현을 봐야 합니다.

원문의 날씨 예제에는 server.py가 없다

환경 설정 조각은 MCP package와 HTTP client를 설치합니다.

1
2
3
4
5
6
7
curl -LsSf https://astral.sh/uv/install.sh | sh

uv init weather-server
cd weather-server
uv venv
source .venv/bin/activate
uv add "mcp[cli]" httpx

이어지는 “서버 구현” 절의 Python 코드는 ClientSession, stdio_client, Anthropic client를 사용하는 MCPClient입니다. 즉, 이름과 import가 보여주듯 server가 아니라 custom client입니다.

연결의 핵심 부분은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def connect_to_server(self, server_script_path: str):
    is_python = server_script_path.endswith(".py")
    is_js = server_script_path.endswith(".js")
    if not (is_python or is_js):
        raise ValueError(
            "서버 스크립트는 .py 또는 .js 파일이어야 합니다"
        )

    command = "python" if is_python else "node"
    server_params = StdioServerParameters(
        command=command,
        args=[server_script_path],
        env=None,
    )

    transport = await self.exit_stack.enter_async_context(
        stdio_client(server_params)
    )
    self.stdio, self.write = transport
    self.session = await self.exit_stack.enter_async_context(
        ClientSession(self.stdio, self.write)
    )
    await self.session.initialize()

이 client는 입력받은 Python 또는 JavaScript file을 child process로 실행하고 stdio session을 엽니다. 하지만 원문에는 weather API를 호출하는 tool definition, input schema, tool handler가 들어 있는 server.py가 없습니다.

Desktop 설정도 존재하지 않는 server file을 가리키는 형태입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": [
        "--directory",
        "/절대/경로/weather-server",
        "run",
        "server.py"
      ]
    }
  }
}

따라서 이 글의 명령과 client code만 복사해 “서울의 현재 날씨”를 물어도 weather tool이 생기지 않습니다. server.py가 resources, tools, prompts 중 무엇을 제공할지 구현하고, 실제 절대 경로와 필요한 credential을 설정해야 합니다. 이 예제는 완전 실행 tutorial이 아니라 연결 구조의 일부입니다.

Tool loop도 한 번 더 호출하는 핵심 조각이다

원문의 client는 server에서 tool schema를 받아 model API에 전달합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
response = await self.session.list_tools()
available_tools = [{
    "name": tool.name,
    "description": tool.description,
    "input_schema": tool.inputSchema,
} for tool in response.tools]

response = self.anthropic.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=1000,
    messages=messages,
    tools=available_tools,
)

model response에 tool_use가 있으면 server를 호출하고 result를 다시 user content로 넣습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
result = await self.session.call_tool(tool_name, tool_args)

messages.append({
    "role": "assistant",
    "content": assistant_message_content,
})
messages.append({
    "role": "user",
    "content": [{
        "type": "tool_result",
        "tool_use_id": content.id,
        "content": result.content,
    }],
})

response = self.anthropic.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=1000,
    messages=messages,
    tools=available_tools,
)

이 코드는 기본 원리를 보여주지만 범용 agent loop로는 불완전합니다.

  • 다음 response가 또 tool_use를 반환해도 반복 처리하는 loop가 없습니다.
  • response.content[0]이 항상 text라고 가정하는 부분이 있습니다.
  • 여러 tool call과 부분 실패를 안전하게 합치는 처리가 없습니다.
  • model이 만든 arguments를 승인이나 별도 validation 없이 server에 넘깁니다.
  • 호출 arguments를 그대로 출력하므로 민감한 값이 log에 남을 수 있습니다.
  • model ID와 SDK interface는 코드 작성 시점의 snapshot이므로 설치 환경과 맞는지 확인해야 합니다.

또한 async def 안에서 동기식 model API를 호출합니다. server tool은 await하지만 model call 동안 같은 event loop에서 다른 일을 처리하는 방식은 별도 설계가 필요합니다.

완전한 client라면 “model 응답 → 모든 tool call 검증, 실행 → 결과 반환”을 종료 조건까지 반복하고, 최대 횟수와 timeout, 취소, 오류별 복구를 둬야 합니다.

로컬 MCP도 data가 외부로 나갈 수 있다

기존 글은 server를 local에서 실행하면 민감한 data가 외부로 전송되지 않는다고 설명했습니다. 그러나 원문의 client code는 local server의 result.content를 Anthropic message에 넣어 model API로 전송합니다. server process의 위치와 model inference의 위치는 별개의 경계입니다.

1
2
3
local MCP server
→ local client/host
→ tool result를 remote model API에 전달할 수 있음

MCP 규격을 사용한다는 사실 자체가 접근 제어를 자동으로 제공하지도 않습니다. server가 file write, database update, message send tool을 노출하면 model이 그 tool을 선택할 수 있습니다. host는 최소 권한과 사용자 승인을 설계해야 합니다.

연결 전에 확인할 항목은 다음과 같습니다.

  1. server command와 script path를 신뢰할 수 있는가
  2. tool마다 읽기, 쓰기, 삭제 권한이 어떻게 다른가
  3. arguments를 schema 외에 domain rule로도 검증하는가
  4. tool result 중 무엇이 remote model로 전달되는가
  5. secret과 personal data가 log에 남지 않는가
  6. 위험한 action 전에 사용자가 대상과 내용을 확인하는가
  7. 호출 시간, 횟수, 비용 제한이 있는가

“명시적 권한 부여”는 host가 구현해야 실제 보호가 됩니다. local stdio transport는 network 공개 범위를 줄일 수 있지만, child process가 가진 file, network 권한까지 자동으로 격리하지는 않습니다.

어디에 쓰면 가치가 있고 어디서 멈춰야 하나

MCP가 잘 맞는 경우는 여러 AI application에서 같은 data source와 tool을 재사용하고 싶을 때입니다.

  • 제품 database를 읽어 최신 marketing 초안을 만드는 workflow
  • warehouse를 query해 지역별 매출 report를 만드는 분석
  • repository와 issue tracker를 읽는 coding assistant
  • CRM과 ticket history를 조회하는 support system

이런 사례에서도 MCP는 연결만 표준화합니다. 매출 합계 검산, repository 변경 review, 고객 data의 접근 범위는 application logic으로 남습니다. tool 수가 적고 한 application에서만 쓰는 단순 integration이라면 MCP server 운영이 얻는 이점보다 복잡성이 클 수도 있습니다.

도입 순서는 작게 잡는 편이 안전합니다.

  1. read-only resource 또는 조회 tool 하나로 시작합니다.
  2. tool schema와 실제 권한을 일치시킵니다.
  3. 어떤 result가 model에게 전달되는지 기록합니다.
  4. 잘못된 arguments와 server failure를 시험합니다.
  5. write action은 사용자 승인과 audit 뒤에 추가합니다.
  6. model을 바꿀 때 같은 tool schema가 실제로 호환되는지 재검증합니다.

MCP의 핵심 가치는 “prompt 한 번으로 완성”이 아니라 “연결 하나를 여러 host에서 이해할 수 있게 만드는 계약”입니다. 표준 interface는 integration의 출발점을 줄여 주지만, data quality, tool correctness, security, final answer의 책임까지 가져가지는 않습니다.

원문과 버전 확인

함께 읽으면 이해가 이어지는 글

자주 묻는 질문

MCP를 쓰면 RAG가 필요 없어지나요?

아닙니다. MCP는 데이터와 도구를 연결하는 규격이고, 어떤 정보를 검색해 답에 사용할지는 RAG와 애플리케이션이 별도로 설계해야 합니다.

로컬 MCP 서버의 결과는 항상 로컬에만 남나요?

아닙니다. 호스트가 원격 모델 API를 사용하면 로컬 도구 결과가 모델 요청에 포함될 수 있으므로 전송 경계와 로그를 따로 확인해야 합니다.

MCP를 처음 도입할 때 어떤 도구부터 시작해야 하나요?

읽기 전용 조회 도구 하나로 시작해 스키마, 오류, 전송 데이터, 시간 제한을 검증한 뒤 사용자 승인이 필요한 쓰기 작업을 추가하는 편이 안전합니다.

THE END / OPSOAI

여기까지 읽었습니다

핵심 장면을 한 번 더 떠올려 보세요. 이해가 남았다면 이 책은 제 역할을 다했습니다.

다른 책 고르기
표지 1

키와 좌우 스와이프를 지원합니다. 읽던 페이지는 이 기기에 저장됩니다.

CONTENTS

이 책의 목차

    8개 장 16 분읽는 시간