개괄적인 흐름
이미 클로드 데스크톱이나 클로드 코드는 한번쯤 사용해 보았을 것이다. 이들이 MCP 컨텍스트에서 어떻게 불리는지 한번 잡고 가면 좋다.
sequenceDiagram participant User as 사용자 participant Client as MCP 클라이언트<br />(클로드 코드, 데스크톱, ...) participant Claude as LLM<br/>(클로드, GPT, ...) participant MCP as MCP 서버 Client->>MCP: 연결 및 초기화 Client->>MCP: 도구 목록 요청 (list_tools) MCP->>Client: 사용 가능한 도구 목록 반환 Client->>Claude: 도구 목록 전달 (시스템 프롬프트에 포함) User->>Client: 질문 입력 Client->>Claude: 질문 전달 Note over Claude: 어떤 도구들을 사용할지 결정 Claude->>Client: 선택한 도구 정보 전달 (복수개 전달 가능) Client->>MCP: 도구 실행 요청 Note over MCP: 실제 도구 실행 (복수개 실행 가능) MCP->>Client: 실행 결과 반환 Client->>Claude: 실행 결과 전달 Note over Claude: 결과를 바탕으로 자연어 응답 생성 Claude->>Client: 생성된 응답 전달 Client->>User: 최종 응답 표시
Mermaid
복사
1.
The client sends your question to Claude
•
MCP 맥락에서 클라이언트는 ‘클로드 코드’나 ‘클로드 데스크톱’ 같은 것들을 의미함
•
이게 가장 헷갈리는 부분
2.
Claude analyzes the available tools and decides which one(s) to use
•
이미 MCP가 연결되는 시점부터 도구 목록들을 가지고 있음
3.
The client executes the chosen tool(s) through the MCP server
•
LLM은 도구를 골라줄 뿐, 실질적인 실행의 주체도 클라이언트임
4.
The results are sent back to Claude
•
클라이언트가 실행했으니, 실행 결과는 클라이언트가 들고 있음. 이를 다시 LLM에 전달
5.
Claude formulates a natural language response
6.
The response is displayed to you!
서버 구현 기본
아래와 같이 구현된 나만의 MCP 서버가 있다.
from fastmcp import FastMCP
mcp = FastMCP()
@mcp.tool()
async def make_ppt(ctx: Context) -> str:
"""PPT를 생성합니다."""
...
if __name__ == "__main__":
mcp.run(
transport="streamable-http",
host="0.0.0.0",
port=8083,
)
Python
복사
서버측 FastMCP 애플리케이션 코드
FastMCP는 Python의 타입 힌트와 독스트링을 분석하여 자동으로 JSON 스키마를 생성한다. 이는 MCP 클라이언트가 list_tools() 호출을 통해 도구의 매개변수, 반환 타입, 설명을 가져갈 수 있게 해 준다.
FastAPI 서버 실행하듯 위 MCP 서버를 실행하고 MCP 세팅 파일에 다음 내용을 작성한다.
{
"mcpServers": {
"myserver": {
"url": "http://0.0.0.0:8083/mcp",
}
}
}
Python
복사
나의 경우에는 클로드 코드를 MCP 클라이언트로 사용해서 빠르게 테스트를 해 보았다. /mcp 명령을 이용하면 클로드 코드에서 해당 mcp를 정상적으로 인식했는지 확인할 수 있다.
> /mcp
Plain Text
복사
정상적으로 인식되었다면 PPT를 만들어 달라는 요청에 해당 도구를 사용할 것인지 승인을 대기하는 프롬프트가 나타나는 것을 확인할 수 있다.
클라이언트 구현 기본
위 서버를 사용하는 클라이언트 코드다.
import asyncio
from fastmcp import Client
from fastmcp.client.logging import LogMessage
config = {
"mcpServers": {
"myserver": {
"url": "http://0.0.0.0:8083/mcp",
}
}
}
client = Client(config)
Python
복사
클라이언트측 FastMCP 애플리케이션 코드
클라이언트 코드에서 myserver MCP 서버에 정의된 ppt 제작 도구(make_ppt)를 호출하려고 한다면 다음과 같이 실행한다. 참고로 MCP 서버를 여러 개 연결하는 상황에서 동일한 이름의 도구들이 다수 있을 수 있으므로, 자동으로 MCP 서버 이름이 도구의 접두어로 추가된다.
async def main():
async with client:
tools = await client.list_tools()
result = await client.call_tool("myserver_make_ppt", {})
print(result)
Python
복사
클라이언트측 FastMCP 애플리케이션 코드
이 코드가 실행되면 MCP 서버 코드가 실행된다. ToolManager이 도구를 찾아 tool 변수에 할당하고, await tool.run(arguments)을 실행한다.
class ToolManager:
"""Manages FastMCP tools."""
...
async def call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult:
"""
Internal API for servers: Finds and calls a tool, respecting the
filtered protocol path.
"""
# 1. Check local tools first. The server will have already applied its filter.
if key in self._tools or key in self._tools_transformed:
tool = await self.get_tool(key)
if not tool:
raise NotFoundError(f"Tool {key!r} not found")
try:
return await tool.run(arguments)
...
Python
복사
서버측 FastMCP 라이브러리 내부
Sampling
sequenceDiagram participant Server participant Client participant User participant LLM rect rgb(255, 255, 200) Note over Server, Client: createMessage Server->>Client: sampling/createMessage end Note over Client, User: Human-in-the-loop review Client->>User: Present request for approval User-->>Client: Review and approve/modify rect rgb(255, 255, 200) Note over Client, LLM: Model interaction Client->>LLM: Forward approved request LLM-->>Client: Return generation end Note over Client, User: Response review Client->>User: Present response for approval User-->>Client: Review and approve/modify rect rgb(255, 255, 200) Note over Server, Client: Complete request Client->>Server: Return approved response end
Mermaid
복사
@mcp.tool
async def generate_summary(content: str, ctx: Context) -> str:
"""Generate a summary of the provided content."""
prompt = f"Please provide a concise summary of the following content:\n\n{content}"
response = await ctx.sample(prompt)
return response.text
Python
복사
우리의 make_ppt 도구가 샘플링 요청을 보내게 되면 어떻게 될까?
@mcp.tool()
async def make_ppt(ctx: Context) -> str:
"""PPT를 생성합니다."""
response = await ctx.sample(PROMPT)
...
Python
복사
서버측 FastMCP 애플리케이션 코드
클로드 코드 클라이언트
먼저 클로드 코드 MCP 클라이언트로 Sampling을 테스트해 보자. 서버는 다음과 같은 오류 메시지를 출력할 것이다.
McpError: Method not found
Python
복사
서버측 FastMCP 오류
위 오류 메시지를 생성한 주체는 MCP 클라이언트, 즉, 클로드 코드다. 왜냐하면 MCP 서버가 클라이언트에 Sampling 요청을 보냈으나(구현: response_or_error = await response_stream_reader.receive()), 클로드 코드는 Sampling 처리를 구현하지 않기 때문에 발생하는 문제다. MCP 클라이언트가 JSONRPCError 스키마에 에러 내용을 담아 데이터를 보내온 것이다(JSON-RPC 프로토콜을 사용하기 때문에 gRPC와 같은 프로토 파일 같은 것은 없음). 그렇다면 클로드 코드의 디버그 모드를 켜고(claude --debug로 실행) 클라이언트에서 집계되는 오류를 한번 확인해보자.
Error: Error calling tool 'make_ppt': Method not found
Python
복사
클라이언트(클로드 코드)측 FastMCP 오류
디버그 모드를 켜도 클로드 코드는 정확히 왜 도구 호출을 실패했는지 정보를 제공하지 않는다. 이 문제는 FastMCP 클라이언트의 경우에는 핸들러가 없는 경우 Sampling not supported 오류 메시지를 전송하도록 구현되어 있으나, 클로드 코드 MCP 클라이언트는 Sampling을 지원하지 않음에도 Sampling not supported 오류 메시지를 전송하도록 구현되어있지 않다 정도로밖에 예측할 수 없다.
직접 구현한 클라이언트
이번에는 우리가 직접 구현한 클라이언트로 Sampling을 사용하는 MCP 서버의 도구를 호출했다. 그 결과 MCP 서버가 클라이언트에 Sampling 요청을 보냈으나, 다음과 같은 오류가 발생한다. FastMCP 클라이언트 구현체는 클로드 코드보다 조금 더 자명하게 Sampling이 구현되어 있지 않아 발생한 오류임을 알려준다.
McpError: Sampling not supported
Python
복사
서버측 FastMCP 오류
이 문제를 해결하려면 MCP 클라이언트에 MCP 서버의 Sampling 요청에 대한 핸들러를 달아주기만 하면 된다.
import asyncio
from fastmcp import Client
from fastmcp.client.logging import LogMessage
async def sampling_handler(messages, params, context):
# Integrate with your LLM service here
response = my_llm(messages)
return reseponse.content # Generated response
config = {
"mcpServers": {
"myserver": {
"url": "http://0.0.0.0:8083/mcp",
}
}
}
client = Client(
config,
sampling_handler=sampling_handler,
)
Python
복사
클라이언트측 FastMCP 애플리케이션 코드
그럼 우리의 make_ppt 도구는 클라이언트를 통해 LLM 호출 결과를 응답으로 받을 수 있다.
@mcp.tool()
async def make_ppt(ctx: Context) -> str:
"""PPT를 생성합니다."""
response = await ctx.sample(MARP_PROMPT)
...
Python
복사
서버측 FastMCP 애플리케이션 코드
MCP 홈페이지 등에는 MCP 서버가 보내 온 요청을 검토하고, LLM의 응답을 MCP 서버로 다시 보내주기 전에 검토를 구현하라고 작성되어 있으나 프로토콜을 통해 강제되는 필수요소는 아니다. 그냥 잘 돌아간다.
sequenceDiagram participant Server participant Client participant User participant LLM Note over Server, Client: createMessage Server->>Client: sampling/createMessage rect rgb(255, 255, 200) Note over Client, User: Human-in-the-loop review Client->>User: Present request for approval User-->>Client: Review and approve/modify end Note over Client, LLM: Model interaction Client->>LLM: Forward approved request LLM-->>Client: Return generation rect rgb(255, 255, 200) Note over Client, User: Response review Client->>User: Present response for approval User-->>Client: Review and approve/modify end Note over Server, Client: Complete request Client->>Server: Return approved response
Mermaid
복사
프로토콜별 서버 연결 방법
stdio 프로토콜을 사용하는 MCP 서버는 mcp.run(transport='stdio')와 같은 코드를 포함한다. stdio 프로토콜을 사용하면 MCP 클라이언트 시작 시 서버 프로세스를 실행하기 때문에, 어떻게 서버 프로세스를 실행할 수 있는지를 MCP 설정 파일에 아래와 같이 명시적으로 지정해야 한다.
{
"mcpServers": {
"myserver": {
"command": "uv",
"args": [
"--directory",
"/Users/solvit/develop/test-mcp",
"run",
"-m",
"main"
]
},
}
}
JavaScript
복사
Streamable HTTP 프로토콜을 사용하는 MCP 서버에는 다음과 같이 연결한다. 기본 포트는 8000이다. mcp.run(transport='http', port=...)과 같은 방식으로 프로토콜과 포트를 지정할 수 있고, config 파일은 다음과 같이 작성한다.
{
"mcpServers": {
"myserver": {
"type": "http",
"url": "http://0.0.0.0:8000/mcp",
}
}
}
JavaScript
복사
parse me : 언젠가 이 글에 쓰이면 좋을 것 같은 재료을 보관해 두는 영역입니다.
1.
None
from : 과거의 어떤 원자적 생각이 이 생각을 만들었는지 연결하고 설명합니다.
1.
None
•
연결한 이유
supplementary : 어떤 새로운 생각이 이 문서에 작성된 생각을 뒷받침하는지 연결합니다.
1.
None
opposite : 어떤 새로운 생각이 이 문서에 작성된 생각과 대조되는지 연결합니다.
1.
None
to : 이 문서에 작성된 생각이 어떤 생각으로 발전되거나 이어지는지를 작성하는 영역입니다.
1.
None
ref : 생각에 참고한 자료입니다.
1.
None