Jump to content

(1부) MCP 실전 쿡북: LangChain에서 네이버 검색 도구 연결하기


Recommended Posts

image.png.25461202de1d54614054c9efce4df6f6.png

들어가며


최근 LLM 기반 서비스의 아키텍처는 외부 도구와 API를 유기적으로 통합하는 방향으로 빠르게 발전하고 있습니다. 이러한 흐름 속에서 등장한 Model Context Protocol(MCP)은 LLM이 외부 서비스와 상호작용하는 방식을 정의한 표준 프로토콜로, 기존 Function calling보다 유연하고 확장 가능한 에이전트 구조를 지원합니다.

또한 Slack, Notion 등 다양한 서비스가 자체 Remote MCP 서버를 제공하기 시작했고, MCP 서버를 손쉽게 생성할 수 있는 빌더와 클라이언트 도구들도 활발히 개발되면서 MCP는 하나의 생태계로 자리잡아가고 있습니다.

본 쿡북은 이러한 흐름에 맞춰, CLOVA Studio에서 제공하는 모델을 MCP 서버와 연동해 에이전트 환경에서 활용하는 방법을 중심으로, 실무에 바로 적용할 수 있는 구현 가이드와 실전 팁을 함께 제공합니다.

 

MCP 실전 쿡북 4부작 시리즈

✔︎ (1부) LangChain에서 네이버 검색 도구 연결하기

✔︎ (2부) 세션 관리와 컨텍스트 유지 전략 링크

✔︎ (3부) OAuth 인증이 통합된 MCP 서버 구축하기 링크

✔︎ (4부) 운영과 확장을 위한 다양한 팁 링크

 

MCP 통신 방식

MCP의 기본 개념과 실행 흐름은 기존 AI 어시스턴트 제작 레시피: FastMCP로 도구 연결하기에서 상세하게 다루었으니 참고 부탁드립니다.

MCP는 호스트(Host), 서버(Server), 클라이언트(Client)가 역할을 나눠 동작합니다. 호스트는 MCP 서버의 실행 환경을 관리하며, 서버는 도구를 정의하고 클라이언트의 요청에 따라 실제 로직을 수행합니다. 클라이언트는 서버를 열고 등록된 도구를 조회하거나 호출하는 역할을 맡습니다. 이렇게 세 구성 요소가 서로 연결되면, 클라이언트와 서버 간의 데이터 교환이 이루어집니다.

이때 MCP에서 데이터를 주고받는 통신 방식은 크게 세 가지로 구분됩니다.

Stdio(Standard I/O)는 표준 입출력 채널을 통해 데이터를 주고받습니다. 설정이 간단하고 빠르지만, 네트워크를 통한 원격 통신이 불가능해 로컬 환경에 한정됩니다. HTTP/SSE(Server-Sent Events)는 HTTP 요청으로 명령을 보내고, SSE 채널을 통해 실시간으로 응답을 받는 구조입니다. 다만, 연결이 끊겼을 때 복구가 어렵고, 최신 프로토콜과의 호환성에 제약이 있어 사용이 권장되지 않습니다. Streamable HTTP는 기존 HTTP 구조를 유지하면서 스트리밍 전송을 지원하는 방식입니다. 단일 엔드포인트에서 요청, 응답, 스트리밍을 모두 처리할 수 있고, 안정성과 확장성이 높습니다. 현재 MCP 공식 스펙에서도 해당 방식을 권장하고 있습니다.

따라서 본 쿡북에서는 Streamable HTTP 방식을 기준으로 MCP 서버와 클라이언트 간 통신을 구성하여, 안정적이고 확장 가능한 에이전트 구현 방법을 살펴볼 예정입니다. 

 

기존 도구와 MCP의 차이점

기존의 Function calling과 MCP는 모두 LLM이 외부 도구(API 등)를 활용해 작업을 수행할 수 있도록 해 준다는 공통점을 지니지만, 구조적 설계와 기능적 역할의 차이로 인해 MCP는 AI와 외부 도구를 보다 유연하게 연동할 수 있는 표준 인터페이스로 주목받고 있습니다. 두 방식을 비교하여 구조적 차이점과 각 방식의 적합한 적용 영역을 살펴보겠습니다.

1. 도구 연동 방식

Function calling에서는 도구가 애플리케이션 프로세스 내부에 내장되어 있습니다. 모델 입력에는 함수 스키마 명세가 포함되며, 모델은 이를 기반으로 호출 명세(JSON)를 생성합니다. 반면 MCP는 도구를 외부 서버 형태로 분리하여 모듈화된 구조를 갖습니다. 클라이언트는 MCP 서버와 통신하면서 호출 가능한 도구와 스키마를 조회하고, 그중 적절한 도구를 동적으로 선택해 호출합니다.

2. 컨텍스트 및 상태 관리

Function calling은 일반적으로 호출 단위가 독립적이므로, 컨텍스트나 상태 유지가 필요할 경우 애플리케이션 측에서 별도로 관리해야 합니다. 반면 MCP는 프로토콜 수준에서 세션 개념을 지원합니다. 클라이언트와 서버가 동일 세션 내 요청을 구분하고, 세션 단위로 상태나 컨텍스트를 유지할 수 있습니다.

3. 권한 제어 및 보안 관리

Function calling에서는 도구 호출과 상태 관리가 모두 애플리케이션 내부에서 이루어지므로, 권한 제어나 인증 로직도 해당 애플리케이션에서 직접 처리해야 합니다. 반면 MCP는 도구가 외부 서버로 분리되어 있기 때문에, 클라이언트–서버 간 호출이 네트워크 경계를 넘을 수 있습니다. 이 과정에서 MCP 서버는 각 도구별 인증 정책과 권한 제어를 서버 단위에서 통합적으로 관리할 수 있으며, OAuth 2.1 기반 인증 흐름 등 표준화된 보안 모델을 적용할 수 있습니다.

4. 적용 가능 영역

Function calling은 명확한 태스크 중심의 단일 호출 환경에 적합합니다. 예를 들어 데이터 추출, 포맷 변환, 단순 분류, 외부 API 조회 등 상태 유지가 필요 없고 지연 시간(latency)이 중요한 경우, Function calling이 빠르고 효율적인 선택일 수 있습니다. 또한 권한 관리 로직이 애플리케이션 내부에 일원화되어 있어 구현 복잡도가 낮습니다. 반면 MCP는 복잡한 프로세스, 다단계 대화, 혹은 도구가 자주 변경되는 환경에서 강점을 발휘합니다. 예를 들어 고객 상담 도중 “주문 확인해 줘” → “언제 배송돼?”처럼 문맥이 이어지는 대화에서는 MCP 서버가 세션 단위로 주문 정보를 유지함으로써 자연스러운 연속 대화가 가능합니다. 또한 도구가 별도의 서버로 분리되어 있으므로 업데이트나 교체가 용이하며, 다양한 시스템과 안전하게 연동할 수 있습니다.

이렇게 Function calling과 MCP의 구조적 차이점으로 인해 MCP가 복잡한 상태 기반 대화와 동적 도구 활용이 필요한 서비스 환경에 강점을 가진다는 점을 확인했습니다. 지금부터 이어지는 쿡북에서는 이러한 강점을 실제 개발 환경에서 구현할  있도록 MCP 핵심 개념부터, 서버와 클라이언트 구현, 세션 기반 컨텍스트 관리, 인증  접근 제어를 실전 예제와 함께 다룹니다

 

쿡북의 목표와 범위

본 쿡북은 단순한 개념 설명이나 데모 시연을 넘어서, 실제 비즈니스와 개발 환경에서 바로 활용할 수 있는 MCP 학습 자료를 제공하는 것을 목표로 합니다.

  • 네이버 검색 API 연동 실습: 네이버 개발자 센터의 Open API를 활용해 MCP 서버를 직접 구성하며, MCP가 어떤 방식으로 외부 서비스를 표준화해 연결하는지 이해합니다.

  • Remote MCP 연동 실습: Remote MCP 서버(Playwright MCP)를 추가로 연동하여, 외부 브라우저 자동화 도구를 통합하고 기능을 확장하는 방법을 실습합니다.
  • Stateful 대화 구현: 단일 요청이 아닌, 이전 대화의 컨텍스트를 유지하며 이어지는 세션 기반 대화 구조를 구현해 봅니다.
  • 표준 인증 적용: OAuth 기반 인증 방식을 실습하여, 실제 서비스 연동 과정에서 필요한 인증 요구 사항을 예제와 함께 구현해 봅니다.

  • 실전 팁과 활용 노하우: MCP를 실제 프로젝트에 적용할 때 도움이 되는 서버 디버깅, 도구 작성 요령 등 실무 중심의 팁을 제공합니다.

 

1. 사전 준비 사항


본 섹션에서는 예제를 실행하기 위해 필요한 도구 및 환경에 대한 준비 과정을 다룹니다.

CLOVA Studio API 준비

CLOVA Studio에서는 Chat Completions, 임베딩을 비롯한 주요 API에 대해 OpenAI API와의 호환성을 지원합니다. 본 예제에서는 OpenAI 호환 API 중 Chat Completions 엔드포인트(/chat/completions)를 활용하며, 상세 호환 정보는 OpenAI 호환성 가이드를 참고하시기 바랍니다. 

또한, 해당 API 호출하려면 CLOVA Studio에서 발급받은 API 키가 필요합니다. API 키 발급 방법은 CLOVA Studio API 가이드에서 확인할 수 있습니다.

 

네이버 개발자 센터 이용 등록

본 예제에서는 네이버 개발자 센터에서 제공하는 OpenAPI를 활용합니다.

API 호출을 위해서는 먼저 네이버 개발자 센터에서 애플리케이션을 등록하고 인증 정보를 발급받아야 합니다.

  • 네이버 개발자 센터에서 애플리케이션을 등록합니다.
    • 애플리케이션을 등록하는 방법은 애플리케이션 등록을 참고해 주세요.
    • 애플리케이션 등록 시 사용 API는 '검색'을 선택합니다.
      • 3부에서는 '네이버 로그인', '캘린더'가 추가로 사용됩니다. 필요에 따라 다른 항목도 함께 선택하세요.
  • API 호출에 필요한 클라이언트 아이디와 클라이언트 시크릿 정보를 확인합니다.
Quote

네이버 API 사용 시 네이버 개발자 센터의 이용 약관을 준수해야 합니다. 약관 준수 의무를 위반하여 발생한 모든 법적 책임은 사용자에게 있습니다. 네이버 API를 사용하는 방법에 대한 자세한 설명은 네이버 개발자 센터 API 공통 가이드를 참고해 주십시오.

 

파이썬 개발 환경 준비

이 예제를 실행하려면 Python 3.10 이상의 개발 환경이 필요합니다. 가상환경을 구성하고 .env 파일에 API 키를 등록한 뒤, 필요한 패키지를 설치합니다.

프로젝트 구성

프로젝트의 전체 파일 구조는 다음과 같습니다. Python 버전은 3.10 이상, 3.13 미만입니다.

mcp_cookbook_part1/
├── server.py
├── client/
│   ├── basic_client.py
│   ├── chat_client.py
│   └── advanced_client.py
└── .env
환경 변수 설정

루트 디렉터리에 .env 파일을 생성한 뒤, 앞서 발급받은 API Key를 다음과 같이 입력하고 저장합니다. 이때 따옴표 없이 값을 작성해야 하며, VS Code에서 실행할 경우 설정에서 Use Env File 옵션이 활성화되어 있는지 확인하세요.

CLOVA_STUDIO_API_KEY=YOUR_API_KEY
NAVER_CLIENT_ID=YOUR_CLIENT_ID
NAVER_CLIENT_SECRET=YOUR_CLIENT_SECRET
패키지 설치

프로젝트에 필요한 패키지 목록은 아래 다운로드 링크에서 확인할 수 있습니다. 해당 내용을 복사해 루트 디렉터리에 requirements.txt 파일로 저장하세요.

http://requirements.txt 다운로드

루트 디렉터리에서 터미널을 실행하여 다음과 같이 예제 실행에 필요한 패키지를 설치합니다. 가상환경 설치를 권장합니다.

# 1. 파이썬 가상환경 생성
python -m venv .venv

# 2. 가상환경 활성화 (macOS/Linux)
source .venv/bin/activate
#    (Windows)
#    .venv/Scripts/activate.ps1

# 3. 패키지 설치
pip install -r requirements.txt

 

시나리오 개요

실습에서 다루는 예제는 다음과 같습니다. MCP를 통해 LLM에 네이버 검색 도구를 연계함으로써, 사용자에게 실시간·최신성이 보장되는 응답을 제공할 수 있도록 합니다.

image.thumb.gif.a80880ffe50b388659fb1a88bd6bc55c.gif

 

2. MCP 서버


본 섹션에서는 MCP 서버를 구성하고 실행하는 방법을 설명합니다.

MCP 서버 개념

MCP 서버는 언어 모델이 외부 데이터 소스에 접근할 수 있도록 돕는 역할을 합니다. 서버에는 세 가지 구성 요소가 있습니다.

  • 도구(Tool): 데이터베이스 조회, REST API 호출 등 구체적인 작업을 수행하는 기능입니다. 도구는 모델이 직접 제어하며, 대화 흐름 속에서 필요한 시점에 클라이언트를 통해 실행됩니다.
  • 리소스(Resource): 파일이나 데이터베이스 레코드처럼 읽기 전용 데이터를 제공하여 모델의 컨텍스트를 보강합니다. 이때, 모델이 직접 리소스 요청에 관여하지는 않고, 사용자 또는 애플리케이션이 필요에 따라 리소스를 조회하고 모델에 전달합니다.
  • 프롬프트(Prompt): 서버의 특정 역할을 설명하는 프롬프트 템플릿입니다. 모델에게 일관된 지침이나 배경 정보를 제공합니다.

MCP는 도구, 리소스, 프롬프트를 모두 명세하고 있지만, 각 클라이언트가 지원하는 범위는 상이합니다. 특히 공식 클라이언트별 지원 기능 목록을 보면, 도구는 공통적으로 제공되는 반면 리소스와 프롬프트를 지원하는 MCP 클라이언트는 상대적으로 드뭅니다.

따라서 예제에서는 모델이 주도적으로 활용할 수 있는 도구(Tool) 중심으로 서버를 구성합니다.

 

MCP 서버 구축

'네이버 개발자 센터 이용 등록'에서 준비한 OpenAPI를 기반으로 MCP 서버를 구성합니다.

아래 코드에서는 네이버 웹 검색 API를 MCP 서버의 도구(web_search)로 등록하여, 사용자가 입력한 검색어를 바탕으로 결과를 조회하고 JSON 형태로 반환합니다. 이렇게 구성된 MCP 서버는 앞서 설명한 세 가지 통신 방식 중 Streamable HTTP로 실행됩니다.

import os
import re
from typing import Annotated, Literal
import httpx
from dotenv import load_dotenv
from pydantic import Field
from fastmcp import FastMCP

# 환경 변수 로드
load_dotenv()
NAVER_CLIENT_ID = os.getenv("NAVER_CLIENT_ID")
NAVER_CLIENT_SECRET = os.getenv("NAVER_CLIENT_SECRET")

# MCP 서버 초기화
mcp = FastMCP("MCP Cookbook Example")

# 네이버 검색 도구 정의
@mcp.tool(
    name="web_search",
    description="네이버 검색을 수행합니다."
)
async def web_search(
    query: Annotated[str, Field(description="검색어입니다. 시점이나 조건 등을 구체적으로 작성하세요.")],
    display: Annotated[int, Field(description="웹 문서를 최대 몇 건까지 검색할지 결정합니다.(1-100)", ge=1, le=100)] = 20,
    start: Annotated[int, Field(description="검색 시작 위치입니다.(1-1000)", ge=1, le=1000)] = 1,
    sort: Annotated[Literal["sim", "date"], Field(description="정렬 옵션입니다. sim=유사도순, date=최신순")] = "sim"
) -> dict:

    """
    네이버 검색 API를 호출해 결과를 구조화하여 반환합니다.

    Args:
        query: 검색어
        display: 검색 결과 수(1~100)
        start: 검색 시작 위치(1~1000)
        sort: 'sim' | 'date'

    Returns:
        {
          "query": str,
          "total": int,
          "items": [{"title": str, "link": str, "description": str}]}
        }
    """
    url = "https://openapi.naver.com/v1/search/webkr.json"
    params = {"query": query, "display": display, "start": start, "sort": sort}
    headers = {
        "X-Naver-Client-Id": NAVER_CLIENT_ID,
        "X-Naver-Client-Secret": NAVER_CLIENT_SECRET,
    }

    async with httpx.AsyncClient() as client:
        r = await client.get(url, headers=headers, params=params)
        r.raise_for_status()
        data = r.json()

    results = []
    for item in data.get("items", []):
        results.append({
            "title": re.sub(r"<.*?>", "", item.get("title") or "").strip(),
            "link": item.get("link"),
            "description": re.sub(r"<.*?>", "", item.get("description") or "").strip(),
        })

    return {
        "query": query,
        "total": data.get("total", 0),
        "items": results,
    }

# 실행
if __name__ == "__main__":
    """
    FastMCP 서버를 streamable-http로 실행합니다.
    접속 URL은 http://127.0.0.1:8000/mcp/ 입니다.
    통신 방식을 변경하려면 `mcp.run()`의 `transport` 인자를 다음과 같이 수정하세요.
        - mcp.run(transport="stdio")
        - mcp.run(transport="sse")
    """
    mcp.run(transport="streamable-http", host="127.0.0.1", port=8000, path="/mcp/")

 

3. MCP 클라이언트


본 섹션에서는 MCP 서버를 LangChain 기반 클라이언트에 연결하고 실행하는 방법을 설명합니다. LangChain에서 제공하는 MCP 어댑터를 사용하면, MCP 서버에 등록된 도구를 LangChain의 도구 인터페이스로 매핑할 수 있습니다. 이를 통해 MCP 도구를 LangChain 에이전트의 일부로 자연스럽게 확장하고, 기존 워크플로에 유연하게 결합할 수 있습니다. 

단일 요청 실행

다음은 HCX-005 모델을 LangChain에 연동하고, MCP 서버에 등록된 도구를 호출하여 최종 응답을 생성하는 단건 실행 코드입니다. 만약 LangChain에서 HCX-007 모델을 도구와 함께 사용하려면 반드시 reasoning_effort="none" 파라미터를 명시해야 합니다.

import os
import asyncio
from dotenv import load_dotenv
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_naver import ChatClovaX
from langchain.agents import create_agent
from langchain.messages import SystemMessage, HumanMessage

async def main(clova_api_key: str, server_url: str):
    """
    CLOVA Studio 모델을 LangChain을 통해 호출하고,
    MCP 서버에 등록된 도구를 연동하여 최종 응답을 생성합니다.

    Args:
        clova_api_key (str): CLOVA Studio에서 발급받은 API Key
        server_url (str): 연결할 MCP 서버의 엔드포인트 URL
    """
    model = ChatClovaX(model="HCX-005", api_key=clova_api_key)

    async with streamablehttp_client(server_url) as (read, write, _,):
        async with ClientSession(read, write) as session:
            # MCP 서버를 초기화하고 도구 목록을 불러옵니다.
            await session.initialize()
            tools = await load_mcp_tools(session)

            # 에이전트를 생성합니다.
            agent = create_agent(model, tools)

            # 대화 상태를 정의합니다.
            state = {
                "messages": [
                    SystemMessage(content="당신은 친절한 AI 어시스턴트입니다. 사용자의 질문에 대해 신뢰할 수 있는 정보만 근거로 삼아 답변하세요."),
                    HumanMessage(content="요즘 날씨에 가기 좋은 국내 여행지 세 곳 알려줘"),
                ]
            }

            # 단건 요청을 실행하고 최종 응답을 출력합니다.
            result = await agent.ainvoke(state)
            print(result["messages"][-1].content)

if __name__ == "__main__":
    """
    .env 파일에서 CLOVA Studio API Key를 로드하고,
    MCP 서버의 엔드포인트 URL을 설정한 후 클라이언트를 실행합니다.
    """
    load_dotenv()
    CLOVA_STUDIO_API_KEY = os.getenv("CLOVA_STUDIO_API_KEY")
    SERVER_URL = "http://127.0.0.1:8000/mcp/"

    asyncio.run(main(CLOVA_STUDIO_API_KEY, SERVER_URL))

위 스크립트를 실행하면, CLOVA Studio 모델(HCX-005)이 MCP 서버를 통해 등록된 도구와 연동되어 사용자의 질문에 응답합니다. 예제에서는 “요즘 날씨에 가기 좋은 국내 여행지 세 곳 알려줘”라는 요청을 입력했으며, 실행 결과 모델이 추석 연휴 일정을 정확하게 정리해 답변을 반환합니다.

아래는 실제 출력된 응답입니다.

Quote

현재 날씨에 가기 좋은 국내 여행지 몇 곳을 다음과 같이 추천드립니다.

- **경기도 가평 아침고요수목원**: 가을에는 다채로운 색깔의 국화와 단풍이 어우러진 모습을 볼 수 있고, 넓은 정원을 산책하며 자연을 만끽할 수 있습니다.
- **제주도 새별오름**: 억새풀이 아름답게 펼쳐진 오름으로 가을의 정취를 느끼기에 아주 좋습니다. 또한 정상에서의 전망이 탁 트여 있어 가슴이 뻥 뚫리는 듯한 느낌을 받을 수 있습니다.
- **강원도 정선 민둥산**: 해발 1119미터의 산으로 정상에는 나무가 없고 참억새 밭이 흐드러지게 피어있어 장관을 이룹니다. 특히 가을에는 억새꽃 축제가 열려 더욱 볼거리가 많습니다.

위의 장소들은 모두 가을의 아름다움을 대표하는 곳으로 개인의 취향에 맞게 선택하시면 됩니다. 하지만 여행 전에 해당 지역의 날씨와 교통상황 등 자세한 정보를 먼저 파악하시기를 권해드립니다. 안전하고 즐거운 여행 되시길 바랍니다!

 

멀티턴 대화 실행

다음은 앞선 단일 요청 코드에서 확장된 형태로, 사용자 입력을 input()으로 반복 처리하며 스트리밍 방식으로 응답을 제공하는 대화 클라이언트입니다. 이 클라이언트는 누적된 대화 히스토리를 참조하여 보다 일관된 멀티턴 대화를 지원합니다.

import os
import asyncio
from dotenv import load_dotenv
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_naver import ChatClovaX
from langchain.agents import create_agent
from langchain.messages import SystemMessage, HumanMessage, AIMessage

async def main(clova_api_key: str, server_url: str):
    """
    CLOVA Studio 모델을 LangChain을 통해 호출하고,
    MCP 서버에 등록된 도구를 연동하여 최종 응답을 생성합니다.

    Args:
        clova_api_key (str): CLOVA Studio에서 발급받은 API Key
        server_url (str): 연결할 MCP 서버의 엔드포인트 URL
    """
    model = ChatClovaX(model="HCX-005", api_key=clova_api_key)

    async with streamablehttp_client(server_url) as (read, write, _,):
        async with ClientSession(read, write) as session:
            # MCP 서버를 초기화하고 도구 목록을 불러옵니다.
            await session.initialize()
            tools = await load_mcp_tools(session)

            # LangGraph 기반 ReAct 에이전트를 생성합니다.
            agent = create_agent(model, tools)

            # 대화 상태를 정의합니다.
            state = {
                "messages": [
                    SystemMessage(content=(
                        "당신은 친절한 AI 어시스턴트입니다."
                        "사용자의 질문에 대해 신뢰할 수 있는 정보만 근거로 삼아 답변하세요."
                    ))
                ]
            }

            print("안녕하세요. 저는 AI 어시스턴트입니다. 원하시는 요청을 입력해 주세요. (종료하려면 '종료'를 입력하세요.)")

            while True:
                user_input = input("\n\nUser: ")
                if user_input.lower() in ["종료", "exit"]:
                    print("대화를 종료합니다. 이용해 주셔서 감사합니다.")
                    break
                
                state["messages"].append(HumanMessage(content=user_input))

                # astream_events를 사용하여 스트리밍으로 응답을 처리합니다.
                try:
                    final_answer = ""
                    
                    async for event in agent.astream_events(state, version="v1"):
                        kind = event["event"]
                        if kind == "on_chat_model_stream":
                            chunk = event["data"]["chunk"]
                            if chunk.content:
                                print(chunk.content, end="", flush=True)
                                final_answer += chunk.content

                        elif kind == "on_tool_start":
                            print(f"\n[도구 선택]: {event['name']}\n[도구 호출]: {event['data'].get('input')}")

                        elif kind == "on_tool_end":
                            print(f"[도구 응답]: {event['data'].get('output')}\n")

                    # 스트리밍이 끝나면 최종 답변을 AIMessage로 만들어 상태에 추가합니다.
                    state["messages"].append(AIMessage(content=final_answer))

                except Exception as e:
                    print(f"\n요청을 처리하는 중에 오류가 발생했습니다. 오류: {e}")
                    pass 

if __name__ == "__main__":
    """
    .env 파일에서 CLOVA Studio API Key를 로드하고,
    MCP 서버의 엔드포인트 URL을 설정한 후 클라이언트를 실행합니다.
    """
    load_dotenv()
    CLOVA_STUDIO_API_KEY = os.getenv("CLOVA_STUDIO_API_KEY")
    SERVER_URL = "http://127.0.0.1:8000/mcp/"

    asyncio.run(main(CLOVA_STUDIO_API_KEY, SERVER_URL))

아래 예시는 사용자가 두 차례 질의를 이어가는 과정에서, MCP 서버의 web_search  도구가 호출되고 누적된 대화 히스토리가 활용되는 흐름을 보여줍니다. 도구 응답의 items 필드는 결과 목록이 길어 중략합니다.

Quote

안녕하세요. 저는 AI 어시스턴트입니다. 원하시는 요청을 입력해 주세요. (종료하려면 '종료'를 입력하세요.)

User: 요즘 날씨에 가기 좋은 국내 여행지 세 곳 알려줘

[도구 선택]: web_search
[도구 호출]: {'query': '가을 날씨에 가기 좋은 국내 여행지', 'display': 20}
[도구 응답]: content='{"query":"가을 날씨에 가기 좋은 국내 여행지","total":2629216,"items":[...]}' name='web_search' tool_call_id='call_aU2RBXFVcomyyT8PBygupAM8'

현재 날씨에 가기 좋은 국내 여행지 몇 곳을 다음과 같이 추천드립니다.

- **경기도 가평 아침고요수목원**: 가을에는 다채로운 색깔의 국화와 단풍이 어우러진 모습을 볼 수 있고, 넓은 정원을 산책하며 자연을 만끽할 수 있습니다.
- **제주도 새별오름**: 억새풀이 아름답게 펼쳐진 오름으로 가을의 정취를 느끼기에 아주 좋습니다. 또한 정상에서의 전망이 탁 트여 있어 가슴이 뻥 뚫리는 듯한 느낌을 받을 수 있습니다.
- **강원도 정선 민둥산**: 해발 1119미터의 산으로 정상에는 나무가 없고 참억새 밭이 흐드러지게 피어있어 장관을 이룹니다. 특히 가을에는 억새꽃 축제가 열려 더욱 볼거리가 많습니다.

위의 장소들은 모두 가을의 아름다움을 대표하는 곳으로 개인의 취향에 맞게 선택하시면 됩니다. 하지만 여행 전에 해당 지역의 날씨와 교통상황 등 자세한 정보를 먼저 파악하시기를 권해드립니다. 안전하고 즐거운 여행 되시길 바랍니다!

User: 해외는?

[도구 선택]: web_search
[도구 호출]: {'query': '가을 날씨에 가기 좋은 해외 여행지', 'display': 20}
[도구 응답]: content='{"query":"가을 날씨에 가기 좋은 해외 여행지","total":2492973,"items":[...]}' name='web_search' tool_call_id='call_kUgCWjOEuX7A2MHd88P9DNAY'

가을 날씨에 가기 좋은 해외 여행지를 다음과 같이 소개합니다.

1. **일본 교토**: 전통적인 일본 문화를 체험하고 아름다운 단풍 구경을 할 수 있습니다. 또한, 선선한 가을 날씨는 축제와 야외 활동을 즐기기에도 좋습니다.

2. **스페인 바르셀로나**: 11월에는 여름철 관광객 수가 줄어들어 더욱 여유로운 관광이 가능합니다. 특히, 가우디 건축물이나 캄프 누 경기장 등 대표적인 관광 명소를 방문하기 좋은 때입니다.

3. **캐나다 밴쿠버**: 캐나다 로키산맥의 가을은 황금빛으로 물든 풍경을 자랑하며, 하이 자연 탐험을 즐기기에 이상적인 장소입니다. 또한, 온화한 기후로 인해 야외 활동하기에 적합한 환경을 제공합니다.

위의 여행지들은 모두 대중적으로 인기있는 여행지로 여행객들에게 많은 볼거리와 즐길 거리를 제공합니다. 하지만 개인의 취향과 선호도에 따라 다른 여행지가 더 적합할 수도 있으니 참고하시기 바랍니다. 또한, 각 국가의 입국 규정과 현지 상황을 미리 확인하여 계획을 세우는 것이 중요합니다.

 

4. 확장 시나리오


이번 장에서는 앞서 구성한 MCP 서버에 Remote MCP 서버를 추가로 연동하여 도구를 확장하는 방법을 다룹니다.

MCP는 모듈화된 구조를 지니기 때문에, 새로운 도구를 추가하거나 기존 도구를 교체하기가 매우 쉬운데요. 특히 네트워크를 통해 Remote MCP 서버로부터 도구를 불러와 즉시 활용할 수 있다는 점이 큰 장점입니다. 최근에는 다양한 서비스 제공자들이 이러한 Remote MCP 서버를 공개하거나 호스팅 형태로 제공하면서 도구 선택의 폭이 더욱 넓어지고 있습니다.

이러한 장점을 활용해, 이번 장에서는 Remote MCP 서버를 연동해 기능을 확장하는 시나리오를 다뤄보겠습니다. 

앞서 사용한 네이버 검색 도구는 다음 예시의 description 필드처럼 본문 일부만 발췌해 제공하기 때문에, 원문의 풍부한 정보를 확보하기에는 한계가 있었습니다.

{
  "items": [
    {
      "title": "[클로바 스튜디오 Cookbook] 랭체인(Langchain)으로 Naive RAG 구현하기",
      "link": "https://blog.naver.com/n_cloudplatform/223974030008",
      "description": "생성까지 RAG 파이프라인을 처음부터 끝까지 직접 구현하며, 참조 링크까지 함께 제공할 수 있습니다. Cookbook에서 코드와... 예제에서는 네이버클라우드플랫폼(NCP)의 클로바 스튜디오 사용..."
    }
  ]
}

이번에는 한 걸음 더 나아가, 검색 결과에 포함된 웹페이지에 접속해 본문까지 직접 탐색하는 방식으로 확장해 보겠습니다. 이를 위해 마이크로소프트에서 제공하는 Playwright MCP 서버를 활용합니다.

 

Remote MCP 서버

Playwright MCP는 마이크로소프트에서 제공하는 브라우저 자동화용 MCP 서버로, 클라이언트가 원격으로 브라우저를 제어하여 웹 페이지를 탐색하거나 콘텐츠를 추출할 수 있도록 지원합니다. 

터미널에서 다음 명령어를 실행하여 Playwright MCP 서버를 시작합니다. 실행된 서버는 http://localhost:8931/mcp를 통해 접근할 수 있습니다. 브라우저 종류나 headless 여부 등 세부 설정은 --config 옵션으로 JSON 설정 파일을 지정해 적용할 수 있습니다. 자세한 옵션은 가이드를 참고하세요.

npx @playwright/mcp@latest --port 8931

 

Quote

본 실습에서 사용하는 Playwright MCP는 브라우저 자동화를 제공하는 도구입니다. 본 예제에서는 오직 테스트 목적으로만 사용하였으며, 실제 서비스에 적용할 경우 각 사이트의 이용 약관 및 정책을 반드시 준수해야 합니다. 또한, 과도한 요청이나 비정상적인 접근은 서비스 제공자 측의 차단 대상이 될 수 있으므로 주의가 필요합니다.

 

MCP 클라이언트

이제 서버 준비가 끝났다면, 클라이언트에서는 서버 URL만 추가하면 바로 도구 연동이 가능합니다. 즉, 앞서 사용하였던 네이버 검색 도구와 함께, 새로운 서버의 엔드포인트(http://localhost:8931/mcp)를 추가하기만 하면, 두 서버를 동시에 사용할 수 있습니다.

또한 필요에 따라 Remote MCP 서버의 도구를 래핑해 커스텀 도구로 구성할 수도 있는데요. 예를 들어, Playwright MCP의 browser_navigate 도구는 HTML 본문 전체를 반환하기 때문에 응답이 매우 길어질 수 있습니다. 이때 불필요한 토큰 소모를 줄이기 위해, 수집한 HTML을 정제하는 기능을 추가한 scrape_and_clean 같은 커스텀 도구를 새롭게 정의할 수 있습니다.

다음 스크립트는 LangChain을 통해 CLOVA Studio 모델을 호출하고, 두 개의 MCP 서버에서 제공하는 도구를 함께 사용하여 답변을 생성합니다. 또한 사용자 입력을 input()으로 반복 처리하며, 스트리밍 방식으로 응답을 제공하는 대화형 클라이언트 형태로 동작합니다.

import os
import asyncio
from dotenv import load_dotenv
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_naver import ChatClovaX
from langchain.agents import create_agent
from langchain.messages import SystemMessage, HumanMessage, AIMessage
from langchain_core.tools import tool
from bs4 import BeautifulSoup
import re

async def main(clova_api_key: str, server_config: dict):
    """
    CLOVA Studio 모델을 LangChain을 통해 호출하고,
    MCP 서버에 등록된 도구를 연동하여 최종 응답을 생성합니다.

    Args:
        clova_api_key (str): CLOVA Studio에서 발급받은 API Key
        server_config (dict): MCP 서버 설정 정보
    """
    model = ChatClovaX(model="HCX-005", api_key=clova_api_key)
    client = MultiServerMCPClient(server_config)

    # 다음과 같이 서버를 바로 연동할 수도 있습니다.
    # agent = create_agent(model, tools)

    # 도구 커스텀을 위해 도구 목록을 직접 불러옵니다.
    tools = await client.get_tools()
    tool_map = {t.name: t for t in tools}

    web_search = tool_map["web_search"]
    browser_navigate = tool_map["browser_navigate"]
    
    # browser_navigate를 래핑하여 새로운 커스텀 도구를 정의합니다.
    @tool
    async def scrape_and_clean(url: str) -> str:
        """주어진 URL로 이동하여 페이지의 HTML을 스크래핑한 뒤 본문 텍스트를 정제합니다."""

        # browser_navigate 도구를 사용하여 페이지 HTML을 가져옵니다.
        html_content = await browser_navigate.ainvoke({"url": url})
        soup = BeautifulSoup(html_content, 'html.parser')
        text = soup.get_text()
        
        # 불필요한 태그, 특수문자, 과도한 공백 등을 제거합니다.
        text = re.sub(
            r'(?:\b[a-z]+(?:\s+\[[^\]]*\])?:\s*|\[[^\]]*\]|[.,\-\|/]{3,}|\s+)',
            ' ',
            text,
            flags=re.I
        ).strip()
 
        return text

    # 도구 목록을 새롭게 정의합니다.
    tools = [web_search, scrape_and_clean]

    # 에이전트를 생성합니다.
    agent = create_agent(model, tools)

    # 대화 상태를 정의합니다.
    state = {
        "messages": [
            SystemMessage(content=(
                "당신은 친절한 AI 어시스턴트입니다.\n"
                "사용자의 질문에 대해 신뢰할 수 있는 정보만 근거로 삼아 답변하세요.\n"
                "만약 정보를 찾기 위해 도구를 사용해야 한다면, 다음 작업 순서에 따라 진행하세요:\n"
                "Step 1: web_search로 검색을 수행합니다.\n"
                "Step 2: 검색 결과 중 가장 관련성이 높은 link에 대해 scrape_and_clean을 호출합니다\n"
                "Step 3: 결과를 확인하고, 다음으로 관련성이 높은 link에 대해 scrape_and_clean을 호출합니다\n"
                "Step 4: 이 과정을 반복하여 여러 link에서 정보를 수집합니다.(최대 3개)\n"
                "Step 5: 수집한 정보를 바탕으로 종합적인 답변을 제공합니다\n"
                "Step 6: **종합적인 답변에는 반드시 link를 명시하여** 정보의 출처를 밝힙니다\n\n"
                "반드시 도구를 하나씩 순차적으로 호출하세요. 동시에 여러 도구를 호출하면 안 됩니다.\n"
                "이미 호출한 도구와 동일한 정보를 다시 요청하지 마세요."
            ))
        ]
    }

    print("안녕하세요. 저는 AI 어시스턴트입니다. 원하시는 요청을 입력해 주세요. (종료하려면 '종료'를 입력하세요.)")
    
    # astream_events를 사용하여 스트리밍으로 응답을 처리합니다.
    while True:
        user_input = input("\n\nUser: ")
        if user_input.lower() in ["종료", "exit"]:
            print("대화를 종료합니다. 이용해 주셔서 감사합니다.")
            break
        
        state["messages"].append(HumanMessage(content=user_input))

        # astream_events를 사용하여 스트리밍으로 응답을 처리합니다.
        try:
            final_answer = ""
            
            async for event in agent.astream_events(state, version="v1"):
                kind = event["event"]
                if kind == "on_chat_model_stream":
                    chunk = event["data"]["chunk"]
                    if chunk.content:
                        print(chunk.content, end="", flush=True)
                        final_answer += chunk.content

                elif kind == "on_tool_start":
                    print(f"\n[도구 선택]: {event['name']}\n[도구 호출]: {event['data'].get('input')}")

                elif kind == "on_tool_end":
                    # 출력이 너무 길어지는 것을 방지하기 위해 browser_navigate 도구의 응답은 출력하지 않습니다.
                    tool_name = event.get("name", "")
                    if tool_name != "browser_navigate":
                        print(f"[도구 응답]: {event['data'].get('output')}\n")

            # 스트리밍이 끝나면 최종 답변을 AIMessage로 만들어 상태에 추가합니다.
            state["messages"].append(AIMessage(content=final_answer))

        except Exception as e:
            print(f"\n요청을 처리하는 중에 오류가 발생했습니다. 오류: {e}")
            pass 

if __name__ == "__main__":
    """
    .env 파일에서 CLOVA Studio API Key를 로드하고,
    MCP 서버의 엔드포인트 URL을 설정한 후 클라이언트를 실행합니다.
    """
    load_dotenv()
    CLOVA_STUDIO_API_KEY = os.getenv("CLOVA_STUDIO_API_KEY")
    SERVER_CONFIG = {
        "search-mcp": {
            "url": "http://127.0.0.1:8000/mcp/",
            "transport": "streamable_http",
        },
        "playwright-mcp": {
            "url": "http://localhost:8931/mcp",
            "transport": "streamable_http",
        },
    }

    asyncio.run(main(CLOVA_STUDIO_API_KEY, SERVER_CONFIG))

위 스크립트를 실행하면 콘솔에서 사용자 질의를 입력해 멀티턴 대화를 진행할 수 있습니다.

외부 정보가 필요한 질의의 경우 먼저 web_search 도구로 관련 링크를 찾고, 우선순위가 높은 링크부터 scrape_and_clean 도구로 탐색합니다. scrape_and_clean은 내부적으로 browser_navigate를 래핑해 사용하므로, 로컬 실행 환경의 Playwright 브라우저가 자동으로 실행되어 페이지를 렌더링하고 HTML을 수집합니다. 수집된 본문 텍스트는 정제된 뒤 도구 응답으로 반환되고, 모델은 이 결과들을 종합해 최종 답변을 생성합니다.

아래는 실제 출력된 응답입니다. 도구 응답의 items 필드 목록과 content 값은 내용이 길어 중략합니다.

Quote

안녕하세요. 저는 AI 어시스턴트입니다. 원하시는 요청을 입력해 주세요. (종료하려면 '종료'를 입력하세요.)

User: RAG 관련된 클로바 스튜디오 Cookbook 찾아서 2개 정도 추천하고 요약 정리해줘

[도구 선택]: web_search
[도구 호출]: {'query': '클로바 스튜디오 RAG Cookbook', 'display': 20, 'sort': 'sim'}
[도구 응답]: content='{"query":"클로바 스튜디오 RAG Cookbook","total":41,"items":[...]}' name='web_search' tool_call_id='call_kbQ0jkNrgqljiU7g2JYoncmx'

[도구 선택]: scrape_and_clean
[도구 호출]: {'url': 'https://www.ncloud-forums.com/topic/422/'}

[도구 선택]: browser_navigate
[도구 호출]: {'url': 'https://www.ncloud-forums.com/topic/422/'}
[도구 응답]: content='...' name='scrape_and_clean' tool_call_id='call_riKcKw3RDt4ZCXb95u5jXAkj'
 
[도구 선택]: scrape_and_clean
[도구 호출]: {'url': 'https://www.ncloud-forums.com/topic/497/'}

[도구 선택]: browser_navigate
[도구 호출]: {'url': 'https://www.ncloud-forums.com/topic/497/'}
[도구 응답]: content='...' name='scrape_and_clean' tool_call_id='call_8JYLtdEwrZXeZHWYQFbecKUw'
 
다음은 두 개의 CLOVA Studio Cookbook 관련 자료에 대한 요약 정리입니다:

1. **"랭체인(Langchain)으로 Naive RAG 구현하기"**
   - [링크](https://www.ncloud-forums.com/topic/422/) 
   - 이 자료는 Langchain과 CLOVA Studio를 연결하여 HTML 가이드를 검색하고 답하는 RAG 시스템을 구현하는 방법을 설명합니다.
   - 주요 내용:
     - 사전 준비: Langchain 패키지 설치, 참조할 문서 준비, 필요한 모듈 import.
     - 데이터 로딩: wget을 사용하여 HTML 데이터를 다운로드하고, LangChain의 Document Loader를 사용해 HTML 파일을 처리 가능한 형태로 변환.
     - 문단 나누기: CLOVA Studio의 문단 나누기 API를 사용하여 문장을 적절한 크기의 청크로 나눔.
     - 임베딩: CLOVA Studio의 임베딩 API를 사용해 텍스트 데이터를 벡터로 변환.
     - 벡터 저장소(Vector Store)에 저장: Chroma 또는 FAISS를 사용하여 벡터 데이터를 저장하고 관리.
     - 검색기(Retriever) 생성: 사용자의 질문에 대해 관련 문서를 검색.
     - RAG 시스템 완성: LCEL을 사용해 체인을 구성하고, 질문에 대한 답변을 생성.

2. **"LangChain으로 이미지가 있는 문서를 검색하는 RAG 시스템 구축하기 (Multimodal RAG Cookbook)"**
   - [링크](https://www.ncloud-forums.com/topic/497/)
   - 이 자료는 클로바 스튜디오와 Langchain을 활용하여 PDF 형식의 데이터에서 텍스트와 이미지를 추출하고, 이를 기반으로 질문에 대한 답변을 생성하는 과정을 다룹니다.
   - 주요 내용:
     - 사전 준비: Langchain 패키지 설치, 필요한 모듈 import, CLOVA Studio API 키 발급.
     - 데이터 전처리: PDF 문서에서 텍스트와 이미지를 추출하고, 이미지를 전처리하여 CLOVA Studio의 비전 모델에 맞게 조정.
     - 이미지 요약: 비전 모델을 사용해 이미지 내용을 텍스트로 요약하고, 이를 문서화.
     - 문서 청킹: 텍스트와 이미지 문서를 의미 단위로 분할.
     - 벡터 데이터 변환 및 저장: 청킹된 문서를 벡터로 변환하고 Chroma 또는 FAISS와 같은 벡터 저장소에 저장.
     - 질의 응답 생성: 사용자의 질문에 대해 문서에서 관련 정보를 검색하고 답변을 생성.

이 두 자료는 CLOVA Studio와 LangChain을 결합해 RAG 시스템을 단계별로 구현하는 실용적인 가이드입니다. 텍스트·이미지 등 다양한 형태의 데이터를 다루는 과정을 통해, RAG의 핵심 구성 요소를 이해하고 실제 환경에서 효과적으로 응용하는 방법을 익힐 수 있을 것입니다.

 

마무리


이번 1부에서는 MCP의 핵심 개념과 구조, 그리고 HyperCLOVA X 모델을 연동한 클라이언트 구축 과정을 살펴보았습니다. 또한 네이버 검색 API와 연동하는 기본 예제를 구현하고, Remote MCP 서버를 추가로 연동하여 도구를 확장하는 시나리오까지 함께 다뤘습니다.

이를 통해 MCP의 모듈화된 구조와 확장성이 실제로 어떻게 활용될 수 있는지를 확인했습니다.

이어지는 2부에서는 세션 관리와 컨텍스트 관리 전략을 다룹니다. 멀티턴 대화 환경에서 MCP를 안정적으로 활용할 수 있는 방법을 소개할 예정입니다.

 

MCP 실전 쿡북 4부작 시리즈

✔︎ (1부) LangChain에서 네이버 검색 도구 연결하기

✔︎ (2부) 세션 관리와 컨텍스트 유지 전략 링크

✔︎ (3부) OAuth 인증이 통합된 MCP 서버 구축하기 링크

✔︎ (4부) 운영과 확장을 위한 다양한 팁 링크

 

 

image.png.b9da587841f5ad3f1b694d81ce098866.png

 

링크 복사
다른 사이트에 공유하기

게시글 및 댓글을 작성하려면 로그인 해주세요.



로그인
×
×
  • Create New...