본문 바로가기
LLM/LangGraph

[LangGraph] Model Context Protocol(MCP)과 LangGraph 커스텀 도구(Custom Tool) 연동 정리

by coding_whale 2026. 6. 16.
반응형

1. 도입부 (Introduction)

LLM(대형 언어 모델)이 아무리 똑똑할지라도, 실시간 웹 정보나 사내 기밀문서 데이터, 혹은 복잡하게 얽혀 있는 한국의 종합부동산세 세율표를 외워서 정확히 연산하는 것은 원천적으로 불가능하다. 결국 에이전트의 성패는 필요한 순간에 올바른 API나 검색 엔진, 데이터베이스를 동적으로 호출하는 '도구 사용(Tool Calling)'의 완성도에 달려 있다.

하지만 수많은 개발자가 겪는 애로사항은 커스텀 도구를 만들 때마다 에이전트 프레임워크마다 서로 다른 스펙으로 수동 조립해야 한다는 점이다. 이 장벽을 허물기 위해 Anthropic은 AI 모델과 개발자가 만든 도구를 표준 규격으로 촘촘히 엮어주는 MCP(Model Context Protocol) 표준을 발표했다.

이번 포스팅에서는 LangGraph에서 노드(Node)와 도구(Tool)가 갖는 본질적인 아키텍처 차이점을 분석하고, 주신 실전 부동산 세금 연산 파이프라인(RAG + Web Search + 수학 연산)을 @tool 기반의 커스텀 도구로 고도화해 보겠다. 나아가 이 커스텀 도구들을 현대 에이전트 인프라의 심장인 MCP 서버(FastMCP)로 패키징하여 배포하는 실전 아키텍처 가이드까지 완벽히 정복해 보자.

 

 

2. 노드(Node) vs. 도구(Tool): 아키텍처적 본질 비교

LangGraph에서 에이전트를 빌드할 때 많은 개발자가 '노드'와 '도구'의 개념적 경계를 혼동하곤 한다. 이 둘은 그래프 상태를 지배하는 방식과 LLM과의 상호작용 측면에서 완전히 다른 책임을 갖는다.

① 그래프의 기본 빌딩 블록, 노드 (Node)

  • 상태 전파의 수문장: 노드는 항상 그래프의 전역 상태(State)를 인자로 받아 연산을 처리하고, 반드시 갱신된 새로운 상태(State)를 딕셔너리 형태로 반환해야 한다.
  • 결정론적 흐름 제어: 상태 변수를 직접 읽고 쓸 수 있기 때문에, 에이전트의 워크플로우를 개발자가 기획한 흐름대로 제어하는 데 사용된다.

 

② 자율적 행동의 실행자, 도구 (Tool)

  • 인메모리 격리: 도구는 전역 그래프 상태(State)를 인자로 직접 받지 않는다. 오직 실행에 필요한 구체적인 매개변수(Arguments)만 인자로 받는다.
  • 독립적 값 반환: 상태 변수를 갱신하는 대신, 함수 본연의 실행 결과(예: 문자열, JSON 객체)만 담백하게 반환한다. 그 결과값을 상태에 누적시키는 일은 도구를 지휘하는 에이전트 노드(Agent Node)나 프리빌트 도구 노드(ToolNode)의 몫이다.
  • LLM을 위한 사용 설명서: 도구는 LLM이 직접 호출 여부를 결정해야 하므로, 함수의 이름과 인자의 타입 힌트, 그리고 도구 설명(Docstring)이 필수적이다. 이 정보들이 컴파일 시점에 JSON 스키마로 번역되어 LLM에게 시스템 지침서로 전달되기 때문이다.

 

 

3. MCP 및 병렬 커스텀 도구 오케스트레이션 설계도

우리가 빌드할 종합부동산세 연산 전용 커스텀 도구들과, 이를 외부 에이전트 환경(Claude Code 등)에 표준 인터페이스로 공급하는 MCP 프로토콜의 협업 흐름을 눈으로 먼저 파악해 보자.

 

 

4. 실전 코드 구현: 정밀 세금 연산 커스텀 도구 및 MCP 서버 조립

주어진 주피터 노트북 소스 코드의 RAG 체인 및 외부 검색 로직을 @tool 표준 데코레이터 형식의 커스텀 도구로 정교하게 다듬어 보겠다. 뒤이어 이 도구들을 원터치로 연동해 주는 FastMCP 서버 배포 코드를 조립한다.

① LangGraph용 커스텀 도구 4종 정의

import os
from datetime import datetime
from langchain_core.tools import tool
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate, ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import AzureChatOpenAI, AzureOpenAIEmbeddings
from langchain_chroma import Chroma
from langchain import hub

# ==========================================
# 1. 인프라 인스턴스화 (Azure OpenAI 기반)
# ==========================================
llm = AzureChatOpenAI(
    azure_deployment='gpt-4o-2024-11-20',
    api_version='2024-08-01-preview',   
    temperature=0
)

small_llm = AzureChatOpenAI(
    azure_deployment='gpt-4o-mini-2024-07-18',
    api_version='2024-08-01-preview',
    temperature=0
)

# 벡터 DB 및 리트리버 구성
embedding_function = AzureOpenAIEmbeddings(
    model='text-embedding-3-large',
    azure_endpoint=os.getenv('AZURE_OPENAI_EMBEDDING_ENDPOINT'),
    api_key=os.getenv('AZURE_OPENAI_EMBEDDING_API_KEY')
)
vector_store = Chroma(
    embedding_function=embedding_function,
    collection_name='real_estate_tax',
    persist_directory='./real_estate_tax_collection'
)
retriever = vector_store.as_retriever(search_kwargs={'k': 3})

# 문서 포맷팅 헬퍼 함수
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# ==========================================
# 2. 커스텀 도구 (Custom Tools) 선언
# ==========================================

# 툴 1: 부동산 공제액 조회 및 계산 도구
rag_prompt = hub.pull('rlm/rag-prompt')
tax_deductible_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | rag_prompt | small_llm | StrOutputParser()
)
user_deduction_chain = (
    PromptTemplate.from_template(
        "아래 [Context]는 주택 종부세 공제액 가이드다. 질문을 기반으로 해당 주택수에 알맞은 공제액 금액만 추출하라.\n\n[Context]\n{tax_deductible_response}\n\n[Question]\n질문: {question}\n답변: "
    ) | small_llm | StrOutputParser()
)

@tool
def tax_deduction_tool(question: str) -> str:
    """사용자의 소유 주택 현황 질문을 분석하여 그에 해당하는 법정 종합부동산세 공제액을 조회합니다.
    
    Args:
        question: 사용자가 소유한 부동산 주택 수 및 세법 질문 (예: '5억, 10억 주택 소유 시 공제액은?')
    """
    # 1단계: RAG 체인으로 일반적인 공제액 정책 조회
    policy_response = tax_deductible_chain.invoke('주택에 대한 종합부동산세 과세표준의 공제액을 알려주세요')
    # 2단계: 유저 질문에 알맞은 최종 공제액 추출
    return user_deduction_chain.invoke({
        'tax_deductible_response': policy_response,
        'question': question
    })


# 툴 2: 종합부동산세 과세표준 산출 공식 조회 도구
tax_base_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | rag_prompt | small_llm | StrOutputParser()
)

@tool
def tax_base_tool() -> str:
    """종합부동산세의 과세표준을 계산하기 위한 수학 공식을 데이터베이스에서 검색하여 반환합니다.
    Args 없음.
    """
    base_question = '주택에 대한 종합부동산세 과세표준을 계산하는 방법은 무엇인가요? 수식으로 표현해서 수식만 반환해주세요'
    return tax_base_chain.invoke(base_question)


# 툴 3: 실시간 웹 검색 기반 공정시장가액비율 도구
from langchain_community.tools import DuckDuckGoSearchRun
search_agent = DuckDuckGoSearchRun()
market_value_rate_chain = (
    PromptTemplate.from_template(
        "아래 [Context]는 실시간 검색된 공정시장가액비율 자료다. 질문에 해당하는 비율 정보만 퍼센트(%) 단독 값으로 축약하라.\n\n[Context]\n{context}\n\n[Question]\n질문: {question}\n답변: "
    ) | small_llm | StrOutputParser()
)

@tool
def market_value_rate_tool(question: str) -> str:
    """현재 연도의 부동산 정책 동향 및 실시간 웹 검색을 결합하여 사용자의 부동산 상황에 알맞은 공정시장가액비율을 반환합니다.
    
    Args:
        question: 사용자의 소유 주택 수에 대한 세법 질문
    """
    # DuckDuckGo 엔진을 활용한 최신 정부 비율 검색
    search_context = search_agent.invoke(f"{datetime.now().year}년도 종합부동산세 공정시장가액비율은?")
    return market_value_rate_chain.invoke({
        'context': search_context,
        'question': question
    })


# 툴 4: 수집된 정보를 바탕으로 최종 종부세를 연산하는 도구
@tool
def house_tax_tool(tax_base_question: str, market_value_rate_question: str, tax_deductible_question: str, question: str) -> str:
    """공식, 공제액, 공정시장가액비율 및 실시간 세율 조회를 결합하여 최종 종합부동산세액 연산 결과 보고서를 도출합니다.
    
    Args:
        tax_base_question: tax_base_tool에서 얻은 과세표준 수식 문자열
        market_value_rate_question: market_value_rate_tool에서 얻은 공정시장가액비율
        tax_deductible_question: tax_deduction_tool에서 얻은 세금 공제액
        question: 최초 사용자의 세금 계산 질문 원본
    """
    house_tax_prompt = ChatPromptTemplate.from_messages([
        ('system', f"""과세표준 계산방법: {tax_base_question}
        공정시장가액비율: {market_value_rate_question}
        공제액: {tax_deductible_question}

        위의 공식과 아래 세율 정보를 바탕으로 단계별 수학 계산 과정을 상세히 풀어 종합부동산세를 산출하라.
        세율표: {{tax_rate}}
        """),
        ('human', '{question}')
    ])
    
    house_tax_chain = (
        {
            'tax_rate': retriever | format_docs,  # 벡터 DB에서 실시간 다주택 세율 수집
            'question': RunnablePassthrough()
        }
        | house_tax_prompt | llm | StrOutputParser()
    )
    return house_tax_chain.invoke(question)

 

② Python FastMCP 서버로 패키징하여 에이전트 인프라에 배포하기

LangGraph 내부에서 수동으로 조립하여 사용하던 위 4종의 정교한 세무 도구들을 다른 외부 MCP 클라이언트(예: Claude Code, Cursor, Windsurf)에서도 마음껏 재사용할 수 있도록 MCP 서버 규격으로 포장해보자.

# mcp_server.py
from mcp.server.fastmcp import FastMCP

# 1. 'RealEstateTaxServer' 명칭의 MCP 서버 인스턴스 초기화
mcp = FastMCP("RealEstateTaxServer")

# 2. 사전에 정의된 커스텀 @tool 함수들을 MCP 서버에 등록한다.
# FastMCP 라이브러리가 함수의 Docstring과 Type Hints를 자동으로 파싱하여 
# JSON-RPC 스키마를 구성하고 Host에 공개한다.
mcp.tool()(tax_deduction_tool)
mcp.tool()(tax_base_tool)
mcp.tool()(market_value_rate_tool)
mcp.tool()(house_tax_tool)

if __name__ == "__main__":
    # stdio 전송 계층을 가동하여 호스트와의 커넥션을 수립할 준비를 마친다.
    mcp.run()

 

 

5. 실무 적용 가이드: Claude Code와의 연동 및 트러블슈팅

작성한 MCP 서버를 터미널 환경의 최강 AI 코딩 에이전트인 Claude Code에 연동하여 실행하는 실무 순서는 다음과 같다.

① MCP 서버 등록 명령어 실행

로컬 환경에 구현된 mcp_server.py를 Claude Code 전역 설정에 추가하여 AI가 상시 스캔할 수 있도록 명령한다.

claude mcp add real-estate-tax --transport stdio python3 /absolute/path/to/mcp_server.py

 

② 트러블슈팅: 도구 스키마 오류 해결 (Before vs. After)

LLM이 도구 인자(Arguments)를 오인하여 잘못된 형식으로 도구를 호출하거나 런타임 에러를 뿜을 때의 가장 결정적인 원인은 Docstring과 타입 힌트의 부실함에 있다.

  • 오류 상황 (Before):
    • 결과: LLM은 question에 문자열을 넣어야 하는지, 주택 수를 딕셔너리로 넣어야 하는지 몰라 도구 사용을 회피하거나 엉뚱한 타입의 값을 전송해 장애(Schema Validation Error)를 유발한다.
  • @tool def tax_deduction_tool(question): # Docstring과 타입 힌트가 아예 없거나 부실함 return "9억 원"
  • 최적화 완료 (After):
    • 결과: 컴파일된 JSON-RPC 스키마가 완벽하게 변환되어, LLM이 도구 매개변수의 의미와 리턴값의 범위를 한 치의 오차도 없이 파악하고 안정적으로 호출에 성공한다.
  • @tool def tax_deduction_tool(question: str) -> str: """사용자의 소유 주택 현황 질문을 분석하여 그에 해당하는 법정 종합부동산세 공제액을 조회합니다. Args: question (str): 사용자가 소유한 부동산 주택 수 및 세법 질문 (예: '5억, 10억 주택 소유 시 공제액은?') Returns: str: 세금 공제액 (예: '9억 원', '12억 원') """ # 명확한 타입 가이드를 통과한 안전한 실행부 ...

 

 

6. 결론: MCP 표준 도입이 가져오는 소프트웨어적 혁신

지금까지 파이썬 기반의 커스텀 RAG, 실시간 웹 검색, 다중 요인 세금 계산기를 랭체인의 @tool 구조체로 변환하고, 이를 최신 에이전트 인터페이스 표준인 Model Context Protocol(MCP) 서버로 융합하는 실전 아키텍처를 총망라해 보았다.

우리가 만든 세금 계산 엔진을 단순한 '챗봇용 프롬프트 시나리오'로 가둬두었다면, 다른 시스템과의 호환성은 원천 차단되었을 것이다. 그러나 이를 독립적인 MCP 서버로 래핑하여 공급함으로써, 우리는 이 세무 전문 도구 세트를 백엔드 그래프 아키텍처에 붙여 사용할 수도 있고, Cursor와 Claude Code 같은 로컬 코딩 비서의 보조 툴로 즉시 이식할 수도 있는 진정한 '지능적 재사용성(Agentic Reusability)'을 확보하게 되었다.

코드를 견고하게 새기는 도구의 원리를 확실하게 이해하고 MCP 프로토콜의 옷을 입혀보자. 우리의 AI 인프라가 세상 모든 레거시 API 및 비즈니스 데이터베이스와 통신하기 위한 강력한 연결 통로가 바로 그곳에서 완성될 것이다.

반응형