MCP 연결

MCP 연결 (Connections)

MCPAdapter는 MCP 연결을 열고, 도구를 발견해, 에이전트가 호출할 수 있는 LangChain 도구로 반환해요. 서버를 어떻게 전달하느냐, 어댑터를 얼마나 오래 열어 두느냐는 앱의 형태에 따라 달라져요. 연결 자체는 FastMCP의 것이고, 이 페이지는 LangChain 패턴을 다루며 트랜스포트·클라이언트 세부 사항은 FastMCP로 연결해 줍니다.

출처: LangChain 공식 문서 — mcp/connections

⚠️ langchain.mcp 네임스페이스는 langchain[mcp]>=1.4.0이 필요하고 현재 베타 상태예요. API는 변경될 수 있어요.

여러 서버에 연결하거나 많은 동시 실행을 배포하지 않는 한, 기본 수명주기를 쓰는 걸 권장해요. 단일 대상이 될 수 있는 것(URL, 스크립트 경로, 인프로세스 서버)은 Transports에서 확인하세요.

패턴 고르기 (Choose a pattern)

각 표에서 한 행씩 고르면 돼요. 서버 형태(server shape)와 연결 수명(connection lifetime)은 독립적이어서, 어떤 형태든 어떤 수명과도 조합할 수 있어요.

서버 형태 (Server shape)

필요한 상황 사용 이동
서버 하나 URL, Path, 또는 인프로세스 대상 Transports
하나의 연결 뒤에 여러 서버 MCPConfig dict MCPConfig
분리된 인증·프로토콜 시대·풀을 가진 여러 서버 ClientGroup ClientGroup

연결 수명 (Connection lifetime)

상황 패턴 이동
스크립트, 노트북, 대부분의 에이전트 async with 안에서 발견한 뒤 종료 연결 수명주기
실행 동안 여러 도구 호출에 걸쳐 한 세션 유지 에이전트 호출 주위에 어댑터 유지 호출당 한 세션
배포에서 많은 동시 실행 실행마다 발견, 공유 풀·캐시 재사용 배포 확장

연결 수명주기 (Connection lifecycle)

MCPAdapter는 비동기 컨텍스트 매니저예요. 진입하면 내부 클라이언트를 연결하고, 종료하면 연결을 해제합니다. 발견(discovery)은 컨텍스트 안에서 일어나지만, 반환된 도구가 클라이언트를 들고 있기 때문에 컨텍스트가 끝난 뒤에도 호출 가능해요.

from langchain.agents import create_agent
from langchain.mcp import MCPAdapter


async def build_agent(target):
    # Discover and build the agent inside the adapter's context. The tools hold
    # the client, so the agent stays usable after the context exits.
    async with MCPAdapter(target) as adapter:
        tools = await adapter.list_tools()
        return create_agent("claude-sonnet-5", tools)

에이전트가 살아있는 동안 어댑터를 열어 둘 필요는 없어요. 세션을 열어 두거나 배포를 확장하는 게 아니라면 이 패턴을 권장합니다.

호출당 한 세션 (One session per invocation)

MCPAdapter가 반환하는 도구는 **재진입 가능(reentrant)**해요. 도구가 호출될 때마다 클라이언트를 열고 MCP 호출을 실행한 뒤 해제합니다. 다른 곳에서 이미 연결을 들고 있든 말든요. 따라서 단일 에이전트 실행은 실행 전체에 걸쳐 세션을 열어 두는 대신, 도구 호출당 하나의 세션을 열고 호출이 반환되면 닫아요. 덕분에 오래 실행되는 에이전트가 도구 호출 사이에 유휴 연결을 붙들고 있지 않게 되고, 발견 컨텍스트가 끝난 뒤에도 도구가 호출 가능한 이유이기도 해요.

여러 호출에 걸쳐 세션을 열어 두고 싶다면, 에이전트를 호출할 때 어댑터의 컨텍스트를 열어 두면 돼요. 재진입 가능한 클라이언트는 두 번째 세션을 여는 대신 기존 연결을 재사용합니다.

여러 서버 (Multiple servers)

하나의 에이전트에 여러 서버의 도구를 주려면, 하나의 통합 연결로 충분할 때는 MCPConfig를, 각 서버가 자기만의 연결을 필요로 할 때(다른 프로토콜 시대, 서버별 인증, 클라이언트별로 구성된 공유 풀)는 ClientGroup을 선택하세요.

MCPConfig로 하나의 통합 연결 (One aggregate connection with MCPConfig)

어댑터에 MCPConfig dict를 주면 여러 서버를 하나의 통합 엔드포인트 뒤에서 연결해요. FastMCP는 모든 도구에 자기 설정 키를 접두사로 붙이므로, 같은 도구 이름을 노출하는 두 서버도 모델에 넘겨지는 목록에서 구분 가능해요.

from langchain.agents import create_agent
from langchain.mcp import MCPAdapter

CONFIG = {
    "mcpServers": {
        "weather": {"command": "python", "args": ["/path/to/weather_server.py"]},
        "calc": {"command": "python", "args": ["/path/to/calc_server.py"]},
    }
}


async def fleet_agent(config):
    async with MCPAdapter(config) as adapter:
        # Every tool is prefixed with its config key (`weather_...`, `calc_...`),
        # so two servers exposing the same tool name stay distinguishable.
        tools = await adapter.list_tools()
        return create_agent("claude-sonnet-5", tools)

각 백엔드는 독립적으로 주소 지정되므로, 플릿은 트랜스포트를 섞을 수 있어요 — 한 서버는 stdio로, 다른 서버는 HTTP로요. 다만 MCPConfig 플릿은 모든 백엔드에서 단일 협상된 프로토콜 시대를 공유해요. 레거시 전용 서버를 추가하면 플릿 전체가 레거시 시대로 떨어집니다.

ClientGroup으로 독립 연결 (Independent connections with ClientGroup)

각 서버를 자기 연결에 두려면 ClientGroup을 전달하세요. 각 구성원은 자기만의 협상된 프로토콜 시대, 인증, 핸들러를 유지하며, 그룹은 각 호출을 그 도구를 광고한 클라이언트로 라우팅해요. 이것이 레거시 서버와 현대 서버를 나란히 실행하게 해주고, 도구를 같은 방식으로 네임스페이싱하므로 서버 간 동일한 도구 이름도 충돌하지 않아요.

from fastmcp.client import Client
from fastmcp.client.group import ClientGroup
from langchain.agents import create_agent
from langchain.mcp import MCPAdapter


async def agent_from_group(legacy_url: str, modern_url: str):
    # One connection per server: a `ClientGroup` keeps each server on its own
    # negotiated protocol era, so a legacy and a modern server run side by side.
    # It also namespaces every tool as `{server}_{tool}`, so two servers exposing
    # the same tool name stay distinct.
    group = ClientGroup(
        {
            "weather": Client(legacy_url, mode="legacy"),
            "calc": Client(modern_url, mode="auto"),
        }
    )
    async with MCPAdapter(group) as adapter:
        tools = await adapter.list_tools()
        return create_agent("claude-sonnet-5", tools)

배포 확장하기 (Scale a deployment)

많은 실행을 서비스하는 배포 환경은 실행마다 발견을 하되 그 아래의 연결은 재사용해야 해요. 요청 때마다 다시 연결하면 안 되죠. langgraph dev 그래프 팩토리 안에서 에이전트를 만들어 각 실행이 현재 도구 카탈로그를 사용하게 하고, 공유 연결 풀과 응답 캐시가 비용을 흡수하게 하세요.

SERVERS = {
    "weather": "http://localhost:8001/mcp",
    "calc": "http://localhost:8002/mcp",
}


async def make_graph():
    """Build an agent over an MCP fleet. Called once per run by `langgraph dev`."""
    config = {"mcpServers": {name: {"url": url} for name, url in SERVERS.items()}}
    # A long-lived deployment discovers per run, but reuses one HTTP connection
    # pool underneath. `cache_mode="use"` serves a cached tool list within the
    # server's TTL instead of re-listing on every run.
    async with MCPAdapter(config) as adapter:
        tools = await adapter.list_tools(cache_mode="use")
        return create_agent("claude-sonnet-5", tools)

langgraph dev 그래프 팩토리에서는 주석 처리된 매개변수 타입과 반환 타입이 TYPE_CHECKING 아래에서뿐 아니라 런타임에 import 가능해야 해요. langgraph-apityping.get_type_hints()로 팩토리를 분류하므로, 주석을 해석하지 못하면 런타임 대신 config dict를 주입합니다.

완전한 배포 예시(각 호출자에게 토큰을 발급하는 사용자별 인증 포함)는 Authentication을 참고하세요. 기본적으로 각 FastMCP 클라이언트는 자기 HTTP 연결을 관리해요. 서버 플릿이나 많은 동시 실행에서는 그게 많은 독립 풀을 의미하죠. 하나의 풀을 공유하려면 단일 트랜스포트에서 끌어오는 httpx_client_factory를 전달하고, 어떤 클라이언트도 닫지 못하게 빌려주면 됩니다.

import httpx2
from fastmcp.client import Client
from fastmcp.client.group import ClientGroup
from fastmcp.client.transports import StreamableHttpTransport
from langchain.mcp import MCPAdapter

# One connection pool, shared by every server the deployment talks to.
_POOL = httpx2.AsyncHTTPTransport()


class _SharedPool(httpx2.AsyncBaseTransport):
    """Lend `_POOL` to each client without letting any client close it."""

    handle_async_request = _POOL.handle_async_request

    async def aclose(self) -> None: ...


def _client_factory(**kwargs: object) -> httpx2.AsyncClient:
    return httpx2.AsyncClient(transport=_SharedPool(), **kwargs)


async def load_over_shared_pool(servers: dict[str, str]) -> list:
    # Every client draws HTTP connections from the same pool, so a fleet of
    # servers does not each open its own.
    group = ClientGroup(
        {
            name: Client(
                StreamableHttpTransport(url, httpx_client_factory=_client_factory)
            )
            for name, url in servers.items()
        }
    )
    async with MCPAdapter(group) as adapter:
        return await adapter.list_tools()

모든 클라이언트가 _POOL을 빌려 쓰므로, 배포 환경은 서버마다 하나씩이 아니라 플릿 전체에 대해 한 벌의 HTTP 연결을 엽니다.

캐싱 (Caching)

FastMCP는 list_tools의 결과를 캐시해서 반복된 발견이 네트워크 왕복을 피하게 할 수 있어요. 캐싱은 선택(opt-in) 사항이고 서버의 자체 캐시 힌트를 존중하므로, 그것을 광고하는 현대 시대 서버에 대해서만 적용됩니다. list_tools()는 구성된 캐시를 어떻게 읽을지 선택하는 cache_mode를 받아요.

  • use (기본값) — 서버의 TTL 힌트 안에 캐시된 도구 목록이 있으면 그걸 서빙하고, 없으면 가져와 저장
  • refresh — 서버에서 새 목록을 가져와 캐시를 다시 채움
  • bypass — 캐시를 완전히 건너뜀
tools = await adapter.list_tools(cache_mode="refresh")

캐시와 사용자별 격리는 클라이언트 자체에서 Client(cache=...)로 구성해요. 복제본 플릿의 공유 스토어나 사용자별 캐시 분할은 FastMCP 문서의 Response caching을 참고하세요.

프로토콜 시대 (Protocol eras)

MCP는 클라이언트와 서버가 각자 지원하는 것을 어떻게 합의하는지가 바뀌었어요. 레거시(legacy) 시대는 모든 연결을 initialize 핸드셰이크로 시작하고, 현대(modern) 시대(프로토콜 버전 2026-07-28 이후)는 서버의 server/discover 엔드포인트를 조사해 지원을 발견해요. FastMCP는 연결별로 시대를 협상하므로, LangChain 쪽에서는 주어진 서버가 어떤 시대를 쓰는지 알 필요가 없어요. 서로 다른 시대의 서버 도구를 한 에이전트에 담으려면, 각 서버에 자기만의 연결을 줘서 그것이 지원하는 최선의 시대를 유지하게 하세요 — ClientGroup을 쓰든 서버당 어댑터 하나를 쓰든요. 사전 빌드된 fastmcp.Clientmode 매개변수로 시대를 선택합니다.

from fastmcp.client import Client


async def agent_across_eras(legacy_target, modern_target):
    # MCP has two protocol eras. FastMCP negotiates per connection, so a
    # separate adapter per server lets each keep the best era its own server
    # supports. `mode="legacy"` pins the handshake era; `mode="auto"` (the
    # default) negotiates the newest the server understands.
    legacy = Client(legacy_target, mode="legacy")
    modern = Client(modern_target, mode="auto")
    async with (
        MCPAdapter(legacy) as legacy_adapter,
        MCPAdapter(modern) as modern_adapter,
    ):
        tools = await legacy_adapter.list_tools() + await modern_adapter.list_tools()
        return create_agent("claude-sonnet-5", tools)

두 서버를 단일 MCPConfig 플릿으로 전달하면 대신 플릿이 가진 모든 것에 대해 시대를 하나로 협상해서, 어떤 구성원이 요구하는 가장 오래된 시대로 모든 서버를 끌어내려요. 전체 협상 규칙은 FastMCP 문서의 Protocol negotiation을 참고하세요.

함께 보기 (See also)

더 알아보기 (Learn more)