LangChain의 MCP: 무상태 프로토콜, 엘리시테이션, 그리고 더 많은 것들!

TMT

https://www.langchain.com/blog/mcp-in-langchain-stateless-protocol-elicitation-and-more

MCP 공식 Tier 1 SDK한 달에 5억 건에 가까운 다운로드를 기록하고 있고, 사용량은 그보다 더 빠르게 늘고 있습니다. ChatGPT 사용자가 일으킨 MCP 도구 호출은 2026년 한 해 동안 98배로 늘었으며, 8월 한 달만 해도 두 배 넘게 증가했습니다.

Image

모델 컨텍스트 프로토콜(MCP)은 에이전트를 도구에 연결하는 가장 널리 쓰이는 방법입니다. 지난 7월, 이 프로토콜은 출시 이후 가장 큰 폭으로 다시 쓰였습니다. 저희는 새 명세와 그 뒤에 있는 늘어나는 수요에 맞춰 LangChain의 MCP 지원을 새로 정비했습니다.

달라진 점은 세 가지입니다.

  • MCP 지원이 메인 패키지 안으로 들어왔습니다. 이제 langchain-mcp-adapters를 따로 설치하지 않고 langchain.mcp에서 씁니다.
  • FastMCP 위에 올렸습니다. 전송 방식과 인증, 연결 관리, 프로토콜 협상을 아래에 깔린 클라이언트가 맡기 때문에, 예전 명세를 쓰는 서버와 새 명세를 쓰는 서버가 모두 동작합니다.
  • 인터럽트를 이용한 엘리시테이션과 클라이언트 쪽 캐싱을 지원합니다. 새 명세는 서버가 호출 도중에 되묻는 엘리시테이션(elicitation)을 다시 시도할 수 있는 한 번의 왕복으로 바꿨고, 저희는 이를 LangGraph 인터럽트로 드러냅니다. 도구 목록도 캐시할 수 있게 되어서, 실행할 때마다 도구 목록을 다시 받아 올 필요가 없습니다.

새로워진 무상태 명세

예전 명세에서는 모든 MCP 호출이 세션을 중심으로 설계된 프로토콜을 거쳤습니다. MCP로 도구를 호출하려면 먼저 세션을 열어야 했습니다. 클라이언트와 서버가 핸드셰이크를 하면 서버가 세션 ID를 돌려주고, 그다음의 모든 요청은 그 ID를 함께 실어 보내야 했습니다. 그래서 클라이언트는 세션 ID를 발급한 서버 인스턴스 하나에 묶여 버렸습니다.

Image

원격 서버를 어느 정도 규모로 운영하려면 스티키 라우팅과 세션 저장소 공유가 필요했습니다.

새 MCP 명세는 이 모든 것을 바꿔서, 상태를 두지 않는 무상태 코어를 가능하게 했습니다. MCP 팀은 무상태 코어를 개발자들이 가장 많이 요청한 기능 가운데 하나로 설명합니다. 서버의 안정성과 확장성을 더 끌어올리고 싶다는 요구였습니다. 새 명세에는 붙잡아 둘 상태가 남아 있지 않습니다. 살아 있는 세션 자체가 없으니, 재배포가 세션을 끊어 버리는 일도 더 이상 없습니다.

Image

이 변화로 두 가지가 열렸고, 둘 다 이제 langchain.mcp에 들어 있습니다.

  • 캐싱: 서버가 자기 도구 목록이 얼마나 오래 유효한지 알려 줄 수 있어서, 클라이언트는 실행할 때마다 목록을 다시 받아 오지 않아도 됩니다.
  • 엘리시테이션: 도구가 잠시 멈춰서 호출한 쪽에 무언가를 물어볼 수 있습니다. 삭제를 확인받거나 모델이 빼먹은 파라미터를 받아 오는 식인데, 답을 기다리는 동안 연결을 열어 둘 필요가 없습니다.

이번 개정에 대한 더 자세한 내용은 MCP 팀의 공지를 읽어 보세요.

일급 기능이 된 MCP 지원

에이전트에서 MCP를 일급 기능으로 쓸 수 있도록 MCP 지원을 langchain으로 옮겼습니다. mcp 엑스트라로 설치합니다.

pip install "langchain[mcp]"

예전 패키지를 쓰고 있었다면, MultiServerMCPClientMCPAdapter 클래스 하나로 합쳐진 것을 보게 됩니다. 어떻게 옮겨 오면 되는지는 마이그레이션 가이드에 더 자세히 정리해 두었습니다. 기본 사용법은 다음과 같습니다.

from deepagents import create_deep_agent
from langchain.mcp import MCPAdapter


async def main():
    async with MCPAdapter("https://example.com/mcp") as adapter:
        agent = create_deep_agent(
            model="google_genai:gemini-3.8-flash", tools=
            await adapter.list_tools()
        )
        return await agent.ainvoke(
            {"messages": [{"role": "user", "content": "..."}]}
        )

여기서 나오는 도구는 평범한 LangChain 도구라서, 도구를 넣을 수 있는 곳이면 어디에든 넣을 수 있습니다. create_deep_agentcreate_agent, 직접 엮은 그래프 모두 가능합니다.

FastMCP 위에 올린 구현

FastMCP는 전송 계층 위에 깔끔한 추상화를 제공합니다. 연결인증, 캐싱, 프로토콜 협상이 여기에 해당합니다. 이 클라이언트가 노출하는 기능은 직접 가져다 쓸 수 있습니다.

이제 MCP 프로토콜에는 뚜렷하게 구분되는 두 '시대'가 있고, 그래서 클라이언트는 서로 다른 두 프로토콜과 협상할 수 있어야 합니다. FastMCP는 이 협상을 연결마다 처리합니다. 새 프로토콜을 먼저 시도해 보고, 아직 올리지 않은 서버라면 예전 핸드셰이크로 되돌립니다. 어느 쪽이든 코드는 그대로입니다. 자세한 내용은 FastMCP 4의 새로운 기능에 있습니다.

ClientGroup으로 서버마다 자기 연결을 하나씩 주면, 각 서버는 자기가 지원하는 가장 최신 시대와 자기 인증 정보를 그대로 유지합니다.

from langchain.mcp import MCPAdapter
from deepagents import create_deep_agent
from fastmcp import ClientGroup

group = ClientGroup(
    {
        # 아직 업그레이드하지 않았으므로 핸드셰이크 시대로 고정. 인증은 OAuth 2.1.
        "billing": Client("https://billing.internal/mcp", mode="legacy", auth="oauth"),
        # 이해할 수 있는 가장 최신 시대로 협상하고, 베어러 토큰을 사용.
        "docs": Client("https://docs.internal/mcp", mode="auto", auth=docs_token),
    }
)

async with MCPAdapter(group) as adapter:
    # billing_search와 docs_search로 나뉘어 둘이 섞이지 않는다.
    tools = await adapter.list_tools()
    agent = create_deep_agent(model="google_genai:gemini-3.8-flash", tools=tools)

도구 이름 앞에는 그 도구가 온 서버 이름이 붙습니다. 그래서 두 서버에 각각 있는 search 도구가 billing_searchdocs_search로 들어옵니다. 서버끼리 설정을 다르게 할 필요가 없다면 평범한 설정 딕셔너리만으로 충분한데, 어느 쪽을 언제 쓰면 되는지는 연결 가이드에서 다룹니다.

클라이언트의 나머지 기능도 직접 가져다 쓸 수 있습니다.

  • 인증: 베어러 토큰, OAuth 2.1 전체 흐름, 머신 대 머신 인증 정보, CIMD, 또는 httpx2.Auth를 구현한 어떤 방식이든
  • 전송 방식: 스트리밍 가능한 HTTP, stdio, 인메모리. 대상에서 자동으로 추론하거나, 헤더와 SSL, 공유 httpx2 풀을 직접 설정
  • 캐싱: 목록 조회 결과를 서버의 TTL이 허용하는 동안 보관
  • 진행 상황과 로그: 오래 걸리는 호출에서 오는 알림

FastMCP는 서버를 직접 만들고 테스트하기도 쉽게 해 줍니다. FastMCP 인스턴스 자체가 어댑터의 대상이 될 수 있어서, 별도 프로세스나 소켓 없이도 에이전트가 같은 프로세스 안에 있는 실제 MCP 서버를 상대로 동작할 수 있습니다.

인터럽트로 처리하는 엘리시테이션

엘리시테이션은 MCP가 사람의 개입을 지원하는 방식입니다. 호출한 쪽에 먼저 무언가를 물어보지 않으면 끝낼 수 없는 도구가 여기에 해당합니다. 무상태 명세는 이것을, 클라이언트가 답을 붙여서 다시 보내는 평범한 요청으로 바꿨습니다. 그래서 이미 쓰고 있는 인터럽트 프리미티브로 지원할 수 있게 되었습니다. 실행이 멈추고, 에이전트의 작업을 검토하는 사람이 답을 주면 실행이 다시 이어집니다.

paused = await agent.ainvoke(
    {"messages": [{"role": "user", "content": "Book a table for 4."}]}, config
)
question = paused["__interrupt__"][0].value.requests[0]

answer = {"action": "accept", "content": {"date": "2026-09-14"}}
result = await agent.ainvoke(
    Command(resume={"responses": {question["key"]: answer}}), config
)

멈춘 실행이 기다릴 곳만 있으면 되니, 체크포인터 말고는 따로 준비할 것이 없습니다. 질문을 거절하는 방법과, 같은 승인 흐름으로 파괴적인 도구를 막아 두는 방법은 엘리시테이션 문서에서 다룹니다.

클라이언트 쪽 캐싱

에이전트를 실행할 때마다 어떤 도구가 있는지 알아내는 일부터 시작합니다. 모델이 무엇이든 보기 전에 도구를 찾는 요청이 한 번 왕복한다는 뜻입니다. 이제 서버가 자기 도구 목록이 얼마나 오래 유효한지 알려 줄 수 있어서, 목록을 캐시에서 바로 내줄 수 있습니다. cache=True를 주면 그 힌트를 존중하는 인메모리 캐시를 쓸 수 있습니다.

from fastmcp import Client
from langchain.mcp import MCPAdapter

client = Client("https://billing.internal/mcp", cache=True)

async with MCPAdapter(client) as adapter:
    # 캐시를 설정하면 "use"가 기본값이다. 서버의 TTL이 유효한 동안에는
    # 캐시에 있는 목록을 내주고, 실제로 받아 온 결과는 저장한다.
    tools = await adapter.list_tools(cache_mode="use")
    agent = create_deep_agent(
        model="google_genai:gemini-3.8-flash", 
        tools=tools
    )

캐시는 클라이언트에 속하므로, 호출하는 주체마다 클라이언트를 하나씩 두면 목록이 서로 섞이지 않습니다. TTL과 공유 저장소, 나머지 캐시 모드는 응답 캐싱을 참고하세요.

시작하기

uv pip install "langchain[mcp]"

이 네임스페이스는 langchain[mcp]>=1.4.0이 필요하고 아직 베타 단계라서 API가 바뀔 수 있습니다. 오늘은 파이썬 지원을 먼저 내놓고, TypeScript 지원도 곧 따라갑니다.

Edit this page

Search the archive

Find a page by title, or search the text inside one.