모델 콘텍스트 프로토콜(Model Context Protocol, MCP) 은 AI 애플리케이션에서 LLM에 콘텍스트를 표준화된 방식으로 제공하는 프로토콜입니다. USB-C 포트가 다양한 주변 기기를 표준화된 방식으로 연결하듯이 MCP는 AI 모델을 다양한 데이터 소스와 도구에 연결합니다.
핵심 목표는 셋입니다.
- LLM과 데이터와 도구 간의 유연하고 표준화된 통합
- 다양한 LLM 제공자 간 쉬운 전환
- 복잡한 에이전트와 워크플로 구축 지원
MCP의 가장 큰 장점은 특정 LLM 제공자나 프레임워크에 종속되지 않는다는 것입니다. 따라서 새로운 개발 방식이나 프레임워크를 선택해 전환하더라도 처음부터 모든 것을 새로 작성하는 것이 아니라 점진적으로 발전시켜갈 수 있습니다.
구조 — 서버와 클라이언트
| 구성 요소 | 역할 |
|---|---|
| MCP 서버 | 다양한 도구·콘텍스트·프롬프트를 클라이언트에 제공하는 중심 허브. 단일 서버가 여러 도구를 동시에 호스팅하며, 로컬 데이터베이스나 웹 API와 직접 연결된다. 전체 구조는 REST API 서버와 유사하다 |
| MCP 클라이언트 | MCP 서버와 직접 통신해 필요한 도구를 선택하고, 입력을 전달한 뒤 결과를 받아오는 역할. “표 형태로 요약해줘”라는 요청을 서버에 전달하면 서버가 ‘표 생성 도구’를 실행해 결과를 돌려준다 |
이 구조 덕분에 도구의 실행 로직은 서버에 집중되고, 클라이언트는 더 간결하고 유연하게 동작할 수 있습니다.
LangGraph가 MCP 클라이언트를 직접 하기 어려운 이유
이 대목이 13장과 곧바로 이어집니다.
LangGraph는 상태 기반 에이전트 흐름을 설계하는 프레임워크로, 각 노드는 결정된 함수나 도구를 호출하는 구조입니다. 반면 MCP 클라이언트는 동적으로 서버로부터 제공되는 도구 목록, 콘텍스트, 프롬프트를 조합해 요청을 만들어야 하며, 이는 LangGraph 내부의 상태 그래프 구조와는 다소 결이 다릅니다. 즉 LangGraph는 외부 세계와의 직접적인 API 통신보다 정해진 함수 실행 흐름에 더 최적화되어 있습니다.
그래서 필요한 것이 어댑터 계층입니다. LangChain은 langchain-mcp-adapters 패키지로 이를 제공합니다. 어댑터는 LangGraph 내부의 요청을 MCP 클라이언트 형식으로 변환해 서버로 전달하고, MCP 서버의 응답을 LangGraph가 이해할 수 있도록 재구성해 에이전트의 상태 흐름에 통합합니다. LangGraph의 상태 머신과 MCP의 RESTful 통신 구조 사이를 매끄럽게 연결하는 다리 역할입니다.
13장에서 에이전트 노드가 ‘220V 그래프와 110V 에이전트 사이의 돼지코’였다면, 여기서는 어댑터가 그래프와 외부 프로토콜 사이의 돼지코입니다.
14.1 MCP 서버 구현
MCP는 파이썬, 타입스크립트, 자바, 코틀린, C# 등 다양한 언어의 SDK를 지원합니다. LangGraph를 파이썬으로 작성했으므로 관리 포인트를 줄이려면 파이썬 SDK를 쓰는 것이 좋습니다.
!uv add -q "mcp[cli]"
from mcp.server.fastmcp import FastMCP
from dotenv import load_dotenv
load_dotenv()
mcp = FastMCP("Demo") # 클라이언트가 서버를 식별할 때 쓰는 이름
@mcp.tool()
def add(a: int, b: int) -> int:
"""두 숫자를 더합니다"""
return a + b
LangGraph의 @tool 데커레이터와 유사하게 @mcp.tool()로 함수를 MCP 도구로 등록합니다. 함수의 독스트링은 도구의 설명으로 사용되고, 함수의 타입 힌트는 입력과 출력의 스키마를 정의합니다.
여기서 실무 경험이 담긴 조언이 나옵니다.
공식 문서에는 앞선 코드와 같이 도구를 작성하지만, 실제 업무에서는 도구의 이름과 설명을 조금 더 명시적으로 작성해주는 편이 좋습니다. 실제로 경험상
name을 명시적으로 입력해주지 않으면, 해당 도구를 찾지 못하는 오류가 발생하는 경우가 종종 있습니다.
@mcp.tool(
name="add", # 도구의 이름 작성
description="두 숫자를 더합니다", # 도구의 설명(역할) 작성
)
def add(a: int, b: int) -> int:
"""두 숫자를 더합니다""" # 독스트링은 남겨두어도 상관없다.
return a + b
MCP 인스펙터
$ mcp dev mcp_server.py
Starting MCP inspector...
Proxy server listening on port 6277
MCP Inspector is up and running at http://127.0.0.1:6274
MCP 인스펙터로 서버 상태를 확인할 수 있습니다. 탭 구성이 실용적입니다.
| 탭 | 용도 |
|---|---|
| Resources | 서버가 제공하는 모든 리소스의 메타데이터(MIME 타입, 설명)를 보여주고, 구독 테스트로 리소스 변경 시 알림 동작을 검증한다 |
| Prompts | 프롬프트 템플릿의 인수와 설명을 확인하고, 직접 인수를 넣어 생성된 메시지를 미리 볼 수 있다 |
| Tools | 도구의 스키마와 설명을 확인하고, 사용자 정의 입력으로 직접 테스트한다 |
| Ping | 연결 상태 확인과 응답 시간 측정 |
| Sampling | 다양한 입력에 따른 서버의 출력 패턴 분석(성능·일관성 테스트) |
| Roots | 서버의 루트 리소스와 기본 구성 확인 |
[Tools] → [List Tools]로 add 도구를 띄우고 a=3, b=5를 넣으면 "8"이 나옵니다. 책은 이를 웹 개발에서 서버 개발자가 스웨거로 API 스펙을 작성해 클라이언트에서 연동 전 테스트하는 것과 유사하다고 설명합니다.
stdio vs Streamable HTTP
MCP 서버는 두 가지 연결 방식을 지원합니다.
| stdio | Streamable HTTP | |
|---|---|---|
| 방식 | 표준 입출력을 이용한 프로세스 간 통신(IPC). 클라이언트가 서버를 서브프로세스로 실행하고 JSON-RPC 메시지를 주고받는다 | HTTP POST/GET + SSE를 조합한 웹 기반 통신. 단일 엔드포인트에서 양방향 통신 |
| 장점 | 구성이 단순하다. 네트워크 설정이나 서버 인프라가 필요 없고, 외부 공격에 노출되지 않아 보안성이 높으며, HTTP 오버헤드가 없다. 빠른 프로토타이핑에 좋다 | 확장성과 유연성. 다중 클라이언트 동시 연결, 원격 접근, 수평적 확장, Last-Event-ID 로 연결이 끊겨도 메시지 재전송과 연결 재개 |
| 한계 | 동일 서버를 여러 클라이언트가 공유할 수 없고 원격 접근이 불가능하다. 클라이언트가 서버 프로세스의 생명주기를 직접 관리해야 한다 | 서버 인프라와 네트워크 설정이 필요하고, HTTP 오버헤드와 리소스 소비가 크며, 보안 위험에 노출되고 연결 불안정에 대비한 오류 처리가 필요하다 |
stdio의 세부 규칙도 짚습니다. 각 메시지는 개행문자로 구분되며, 직렬화된 JSON 메시지 중간에 실제 개행문자가 포함되어서는 안 됩니다. 서버는 로깅 목적으로 표준 오류(stderr)에 UTF-8 문자열을 출력할 수 있지만, 표준 출력에는 유효한 MCP 메시지만 전송해야 합니다.
Streamable HTTP를 쓴다면 보안이 필수
MCP 서버가 외부로 노출되므로 보안 조치가 필수입니다.
- 가장 중요한 조치는 DNS 리바인딩 공격 방지 — 서버는 모든 들어오는 연결의
Origin헤더를 검증하고 허용된 도메인 목록을 관리해야 한다. - 로컬 실행 시
localhost(127.0.0.1)에만 바인딩한다. 모든 네트워크 인터페이스(0.0.0.0)에 바인딩하는 것은 외부 공격에 노출될 위험이 있어 피해야 하며, 개발 환경에서 자주 간과하는 부분이다. - 모든 연결에 인증을 요구하고, 세션 ID는 UUID나 JWT 같은 암호학적으로 안전한 방식으로 생성한다. 만료 시간과 갱신 정책도 세운다.
- 프로덕션에서는 반드시 HTTPS로 통신을 암호화한다.
그래서 책의 권장은 명확합니다.
별도의 인프라를 구성하지 않는다면, 에이전트와 같은 프로젝트에 MCP 서버를 구현하고 stdio 방식을 사용해 연결하는 것을 권장합니다. 별도의 인프라를 구축하면 다양한 에이전트가 같은 MCP 서버를 활용할 수 있다는 장점이 있지만, 보안을 위한 인증·인가 등의 정책이 추가되어야 하고 유지 보수할 관리 포인트가 증가하기 때문입니다.
추후 Streamable HTTP로 전환하더라도 일단 stdio로 로컬에서 테스트해보고 이전하는 것을 권합니다. 개발 복잡도를 단계적으로 관리하고, 기능 검증과 보안 구성을 분리해 진행할 수 있기 때문입니다.
if __name__ == "__main__":
mcp.run(transport="stdio") # stdio 또는 streamable-http
14.2 LangChain 도구를 MCP 도구로 변환
12장에서 만든 종합부동산세 에이전트의 도구 중 웹 검색으로 공정시장가액비율을 계산하는 get_market_value_rate 를 옮깁니다. 방법은 간단합니다. mcp_server.py에 코드를 붙여 넣고 @tool을 @mcp.tool()로 바꾸고, 이름과 설명을 데커레이터 안에 작성하면 됩니다.
from langchain_community.tools import DuckDuckGoSearchRun
def get_market_value_rate_search():
"""올해의 공정시장가액비율을 찾기 위해 웹 검색을 수행합니다."""
search = DuckDuckGoSearchRun()
return search.invoke(f"{datetime.now().year}년도 공정시장가액비율은?")
@mcp.tool(
name="get_market_value_rate",
description="""사용자의 부동산 상황에 적용되는 공정시장가액비율을 결정합니다.
이 도구는:
1. 현재 공정시장가액비율 정보가 포함된 검색 결과를 사용
2. 사용자의 특정 상황(보유 부동산 수, 부동산 가치)을 분석
3. 적절한 공정시장가액비율을 백분율로 반환
...""",
)
def get_market_value_rate(question: str) -> str:
...
인스펙터에서 “집이 8억일 때 세금은 얼마인가요?”로 테스트하면 "공정시장가액비율 43%" 가 나옵니다.
에이전트에 붙이기
바꿀 곳은 도구 리스트를 담은 tool_list 선언 부분뿐입니다.
!uv add -q langchain-mcp-adapters
from langchain_mcp_adapters.client import MultiServerMCPClient
mcp_client = MultiServerMCPClient({
"house_tax_tools": {
"command": "python",
"args": ["./mcp_server.py"],
"transport": "stdio",
},
})
from langgraph.prebuilt import ToolNode
mcp_tool_list = await mcp_client.get_tools() # MCP 서버의 도구
tool_list = mcp_tool_list + [get_tax_deduction, get_tax_base, get_house_tax] # + 로컬 도구
llm_with_tools = llm.bind_tools(tool_list)
tool_node = ToolNode(tool_list)
결과를 보면 MultiServerMCPClient가 MCP 서버와 LangGraph 에이전트를 연결하기 때문에, MCP 서버에서 불러온 도구들과 로컬에 선언한 도구들이 모두 StructuredTool 형식으로 통일됩니다. 어댑터가 제 역할을 한다는 증거입니다.
비동기라서 생기는 제약
여기서 실무적인 함정이 하나 있습니다.
MCP 서버에 있는 도구를 불러올 때
await를 활용하기 때문에 graph에서.stream()메서드를 사용할 수 없다는 것입니다. MCP 서버와의 통신은 비동기 I/O 작업으로 이루어지며, 파이썬의 동기 함수에서는 비동기 코드를 직접 호출할 수 없기 때문입니다.
그래서 .astream() 을 써야 하는데, 이건 일반 제너레이터처럼 반복문으로 순회할 수 없어 async for를 쓰는 별도의 유틸리티 함수가 필요합니다.
async def process_stream(stream_generator):
results = []
try:
async for chunk in stream_generator:
key = list(chunk.keys())[0] # 청크의 키는 그래프의 노드 이름('agent', 'tools')
if key == 'agent':
agent_message = chunk['agent']['messages'][0]
# content가 비어 있으면 도구 호출을 준비하는 상태다.
content = agent_message.content if agent_message.content != '' \
else agent_message.additional_kwargs
print(f"'agent': '{content}'")
elif key == 'tools':
for tool_msg in chunk['tools']['messages']:
print(f"'tools': '{tool_msg.content}'")
results.append(chunk)
return results
except Exception as e:
# 네트워크 오류, 타임아웃 등의 상황에서도 안정적으로 처리한다.
print(f"Error processing stream: {e}")
return results
stream_generator = graph.astream({'messages': [HumanMessage(content=question)]})
all_chunks = await process_stream(stream_generator)
이러한 비동기 처리 방식은 MCP 서버와의 통신 지연이 발생하더라도 애플리케이션이 블로킹되지 않고 효율적으로 동작할 수 있게 해줍니다. 특히 여러 MCP 서버와 동시에 통신해야 하는 상황에서 장점이 두드러집니다.
그래서 MCP를 써야 하나
책은 여기서 균형을 잡습니다.
MCP가 최근 많은 주목을 받는 것은 사실이지만, 모든 상황에서 MCP를 활용해야 하는 것은 아닙니다. LangGraph로 에이전트를 구현할 때는 LangGraph에서 제공하는 내장 도구를 활용하는 것만으로도 충분한 경우가 많습니다. MCP의 진정한 장점은 확장성에 있습니다.
기술적 트레이드오프
- 프로세스 간 통신 오버헤드: MCP 서버는 별도의 프로세스로 실행되기 때문에 간단한 도구를 호출할 때는 네이티브 LangGraph 도구보다 10~50ms 정도의 추가 지연이 발생할 수 있다.
- 직렬화 비용: MCP 프로토콜을 준수하려면 추가적인 직렬화와 역직렬화가 필요하며, 대용량 데이터를 처리할 때 성능 저하의 원인이 될 수 있다.
도입 시나리오별 권장
| MCP 도입이 권장되는 경우 | LangGraph 내장 도구가 적합한 경우 |
|---|---|
| 여러 AI 프레임워크(LangGraph, OpenAI SDK, 구글 A2A 등)를 동시에 사용하는 조직 | 단일 프로젝트나 PoC 개발 |
| 도구의 재사용성과 표준화가 중요한 엔터프라이즈 환경 | 실시간 성능이 중요한 애플리케이션 |
| 외부 파트너나 고객에게 도구를 제공해야 하는 경우 | 팀 내부에서만 사용하는 특화된 도구 |
| 도구의 버전 관리와 독립적인 배포가 필요한 경우 | 빠른 개발과 간단한 아키텍처가 우선시되는 경우 |
단계적 마이그레이션
무조건 MCP로 전환하기보다 팀의 전략이나 방향에 따라 적합한 방식을 선택하는 것이 바람직합니다. 현재의 요구 사항을 충족하는 데 LangGraph의 내장 도구만으로 충분하다면 굳이 MCP로 전환할 필요는 없습니다.
실용적인 접근은 단계적 마이그레이션입니다. 가장 자주 사용하고 여러 시스템에서 공유될 가능성이 높은 핵심 도구부터 MCP로 전환하고, 프로젝트 특화 도구들은 LangGraph 네이티브로 유지하는 하이브리드 방식을 고려할 수 있습니다.
그럼에도 검토할 값어치
2025년 3월 OpenAI가 공개한 에이전트 SDK와 2025년 4월 구글이 공개한 A2A는 모두 MCP 연동을 지원합니다. 만약 LangGraph로만 개발했다면, 이러한 프레임워크에서 활용할 도구들은 호환성을 고려해 다시 개발해야 하는 상황이 발생할 수 있습니다.
장기적인 확장성과 다양한 프레임워크와의 호환성을 고려한다면, MCP 기반의 도구 개발을 검토해볼 값어치가 있습니다. 특히 AI 에이전트 생태계가 빠르게 발전하고 있는 현시점에서 표준화된 도구 인터페이스를 갖추는 것은 미래의 기술 변화에 대응하는 현명한 투자입니다.
정리
- MCP는 AI 모델을 도구·데이터에 연결하는 USB-C다. 특정 LLM 제공자나 프레임워크에 묶이지 않는 것이 최대 장점이다.
- LangGraph는 정해진 함수 실행 흐름에 최적화되어 있어 동적인 MCP 클라이언트 역할을 직접 하기 어렵다. 그 틈을
langchain-mcp-adapters가 메운다. @mcp.tool()에는name과description을 명시한다. 안 그러면 에이전트가 도구를 못 찾는 일이 생긴다.- 기본값은 stdio다. 단순하고 안전하며 인프라가 필요 없다. Streamable HTTP는 확장성을 주는 대신 Origin 검증·127.0.0.1 바인딩·인증·HTTPS를 요구한다.
- 어댑터를 거치면 MCP 도구와 로컬 도구가 모두
StructuredTool로 통일된다. 바꿀 코드는tool_list한 줄이다. - MCP를 쓰면 비동기가 된다.
.stream()대신.astream(), 그리고async for용 유틸리티 함수가 필요하다. - 비용은 10~50ms 지연과 직렬화 오버헤드다. 실시간 성능이 중요하거나 PoC라면 내장 도구가 낫다.
- 전환은 전부가 아니라 공유될 핵심 도구부터. 하이브리드가 현실적이다.