콘텐츠로 이동

클라이언트 모범 사례 (Client Best Practices)

MCP 호스트 애플리케이션(가령 에이전트)이 더 많은 MCP 서버에 연결하고 수백, 수천 개의 도구에 접근하게 되면, 단순한 도구 관리 방식은 한계에 부딪힙니다. 모든 도구 정의를 처음부터 모델의 컨텍스트 윈도우에 넣는 방식은 토큰을 낭비하고, 지연을 늘리고, 모델 성능까지 떨어뜨리죠.

이 문제를 풀기 위한 두 가지 패턴이 있습니다. 점진적 발견(progressive discovery)언제 도구 정의가 컨텍스트에 들어가는지를 제어하고, 프로그래매틱 도구 호출(programmatic tool calling)은 도구가 어떻게 호출되는지를 제어합니다.

점진적 도구 발견 (Progressive Tool Discovery)

단순한 MCP 호스트 구현은 연결된 모든 서버의 도구 정의를 대화가 시작될 때 모델에 그대로 넘겨줍니다. 도구가 몇 개뿐이라면 이 방식이 전혀 문제없어요.

하지만 호스트가 수십 개 서버에 접근하고 그 서버들이 수백 개 도구를 노출한다면, 모델이 사용자 메시지를 읽기도 전에 도구 정의만으로 컨텍스트 윈도우 대부분을 차지할 수 있습니다.

처음부터 전부 넣는 방식은 정의만으로 약 150,000 토큰을 소모하는 반면, 점진적 발견은 작업에 필요한 것만 로드해서 약 2,000 토큰만 사용합니다.

점진적 발견은 이런 문제를 피합니다.

  • 호스트는 tools/list로 도구 정의를 평소처럼 가져오되, 모델의 컨텍스트에 주입하는 것은 미뤄둡니다.
  • 호스트는 모델에게 가벼운 search_tools 메타 도구를 제공합니다.
  • 호스트는 필요할 때만 전체 정의를 컨텍스트에 로드합니다.

점진적 발견을 언제 쓸까

점진적 발견이 가장 유용한 때는 도구 정의가 컨텍스트 윈도우의 큰 부분을 차지할 때입니다. 도구 정의가 컨텍스트의 작은 부분만 차지하는 소규모 도구 세트라면 모든 도구를 로드하는 게 괜찮아요.

도구 정의가 사용 가능한 컨텍스트 윈도우의 상당 부분을 차지하기 시작하면, 클라이언트는 점진적 발견으로 전환해야 합니다. 클라이언트가 언제 전환할지를 정하는 임계값(threshold)을 구현할 것을 권장합니다.

  • 컨텍스트 윈도우의 백분율로 임계값을 정합니다. 예를 들어 1%~5%입니다.
  • 도구 정의를 로드합니다. 임계값에 도달하면 점진적 발견으로 전환합니다.

발견 전략 선택하기 (Choosing a Discovery Strategy)

예를 들어 OpenAIAnthropic은 이 기능을 기본적으로 지원합니다. 사용하는 제공자의 문서에서 이에 해당하는 기능이 있는지 확인해 보세요.

점진적 발견 사용하기 (Using Progressive Discovery)

계층 2: 검사(Inspect). 모델이 후보 도구를 식별하면, 그 도구 하나에 대해서만 전체 정의(입력 스키마, 출력 스키마, 문서)를 가져옵니다.

// 모델은 필요한 도구만 검사합니다
get_tool_details({ name: "salesforce_updateRecord" });

동적 서버 관리 (Dynamic Server Management)

  1. 사용 가능한 서버와 각 서버의 높은 수준의 설명을 담은 레지스트리(registry)를 유지합니다.
  2. 모델이 그 서버의 기능이 필요하다고 판단할 때만 서버에 연결합니다.
  3. 현재 작업에 더 이상 관련 없는 서버는 연결을 끊어 컨텍스트를 확보합니다.
sequenceDiagram
    participant Model
    participant Host
    participant Registry
    participant Server

    Model->>Host: search_available_servers("CRM")
    Host->>Registry: Query available servers
    Registry-->>Host: Salesforce server (not connected)
    Host-->>Model: Salesforce server available

    Model->>Host: enable_server("salesforce")
    Host->>Server: server/discover
    Server-->>Host: Supported versions + capabilities
    Host->>Server: tools/list
    Server-->>Host: Tool definitions
    Host-->>Model: Salesforce server connected

    Note over Model: Task complete

    Model->>Host: disable_server("salesforce")
    Host-->>Model: Server disconnected, context freed

이 방식은 사용자의 의도가 처음부터 알려지지 않는 범용 에이전트에서 특히 잘 작동합니다. 에이전트는 항상 켜져 있는 소수의 서버로 시작하고, 필요에 따라 다른 서버에 연결하죠.

구현 지침 (Implementation Guidelines)

지침 근거
도구 정의 캐시하기 서버에서 한 번 가져온 정의는 호스트 쪽에 메모이즈(memoize)해서, 나중에 다시 주입할 때 tools/list 왕복을 또 하지 않게 합니다. 이것은 현재 모델의 컨텍스트에 있는 것과는 별개입니다.
list_changed 시 새로고침 서버가 notifications/tools/list_changed를 보내면 검색 카탈로그를 다시 색인합니다.

프로그래매틱 도구 호출 / 코드 모드 (Programmatic Tool Calling / Code Mode)

직접 도구 호출을 쓰면 모든 도구 호출이 왕복(round trip)이 됩니다. 모델이 도구 호출을 생성하고, 클라이언트가 실행하고, 전체 결과가 다시 모델의 컨텍스트로 흘러 들어오죠.

여러 도구를 연결해야 하는 작업(문서를 읽고, 변환하고, 다른 곳에 쓰는 등)에서는 중간 결과마다 모델을 통과해서, 모델이 그 결과와 관련이 없을 때조차 토큰을 소모하고 지연을 늘립니다.

프로그래매틱 도구 호출(때로는 "코드 모드"라고도 함)은 클라이언트가 도구 호출을 조합(compose)할 수 있는 방법을 제공합니다. 도구를 직접 호출하는 대신, 모델은 도구를 호출하는 코드를 작성합니다. 코드는 샌드박스 환경에서 실행되고, 최종 결과만 모델로 돌아옵니다.

프로그래매틱 도구 호출은 강력하고 MCP 도구·리소스를 더 효율적으로 쓰게 해주지만, 클라이언트가 샌드박스 환경을 구현해야 합니다.

직접 호출은 모든 중간 결과를 모델에 통과시킵니다(약 100K+ 토큰). 프로그래매틱 호출은 약 200토큰의 스크립트를 샌드박스로 보내고, 샌드박스가 도구 호출을 실행해서 약 15토큰의 요약을 반환합니다.

어떻게 동작하나 (How It Works)

호스트는 MCP 도구 스키마를 샌드박스 안에서 사용 가능한 타입이 있는 API로 변환합니다. 모델이 도구가 필요하면 스크립트를 작성하고 실행합니다.

1단계: MCP 스키마에서 프로그래매틱 API 생성. 호스트는 각 서버의 도구 정의를 읽고, 각 도구의 인자와 outputSchema를 바탕으로 타입이 있는 함수를 만듭니다.

// Logging MCP 서버의 도구 스키마에서 자동 생성됨
interface LogEntry {
  timestamp: string;
  message: string;
  level: string;
}

function logging_getLogs(input: {
  level: "error" | "warn" | "info";
  since: number;
}): Promise<{ entries: LogEntry[] }> {
  return mcp.callTool<{ entries: LogEntry[] }>("logging_getLogs", input);
}

// Ticketing MCP 서버의 도구 스키마에서 자동 생성됨
function ticketing_createIssue(input: {
  title: string;
  body?: string;
  priority: "low" | "medium" | "high";
}): Promise<{ issueId: string }> {
  return mcp.callTool<{ issueId: string }>("ticketing_createIssue", input);
}

MCP 서버는 각 도구에 선택적인 outputSchema를 제공할 수 있습니다. 출력 스키마가 있으면 호스트는 정확한 반환 타입(위의 LogEntry처럼)을 만들어낼 수 있어요.

출력 스키마가 없을 때는 단순한 경로를 권장합니다.

  • 제네릭 타입을 쓰고 넘어갑니다. anystring을 받아들이고 구조화되지 않은 출력을 다운스트림에서 처리합니다. 진짜 해결책은 서버 작성자가 outputSchema를 제공하는 것입니다.
  • 빠른 모델로 타입 있는 결과를 추출합니다. 루프 밖의 단발성 호출에 씁니다.

MCP 도구 호출과 같은 스텁(stub) 가로채기 경로를 통해 호스트가 중개하는 extract(value, ExpectedType) 헬퍼를 노출해서, 샌드박스 자체는 네트워크 연결을 열지 않게 합니다. 이 헬퍼는 값을 ExpectedType으로 강제 변환하기 위해 작은 모델(예: Claude Haiku나 Gemini Flash)로 라우팅합니다.

이 방식은 호출마다 지연이 추가되고 환각(hallucinate)을 일으키거나 필드를 누락할 수 있으므로, 사용 전에 결과를 ExpectedType에 대해 검증하세요.

2단계: 모델이 이 API에 대해 코드를 작성. 사이에 전체 결과가 컨텍스트를 오가는 별도의 도구 호출을 하는 대신, 모델은 단일 스크립트를 작성합니다.

"지난 한 시간 동안의 모든 오류 로그를 찾아 각각의 고유한 오류마다 티켓을 생성해라" 같은 작업을 생각해 보죠. 직접 도구 호출로 하면 수천 개의 로그 항목이 모델의 컨텍스트를 통과하게 됩니다. 코드로 하면 모델이 샌드박스 안에서 필터링합니다.

// 모델이 생성한 코드, 샌드박스에서 실행됨
const logs = await logging_getLogs({
  level: "error",
  since: Date.now() - 3600000,
});

// 모델의 컨텍스트가 아닌 샌드박스 안에서 필터링하고 중복 제거
const uniqueErrors = new Map<string, LogEntry>();
for (const log of logs.entries) {

3단계: 샌드박스가 코드를 실행. 샌드박스 안의 함수 호출은 가로채져서 호스트 브로커를 통해 적절한 MCP 서버로 라우팅됩니다. 로그 데이터와 티켓 생성은 모델의 컨텍스트에 들어가지 않은 채 서버 간에 직접 흐릅니다.

console.log 출력, 즉 단일 요약 줄만 모델로 돌아옵니다.

샌드박스 선택하기 (Choosing a Sandbox)

어떤 샌드박스를 쓰든 통합 패턴은 같습니다. 호스트가 함수 스텁을 주입하고, 인프로세스(in-process)나 stdio 채널을 통해 호출을 가로채며(그래서 네트워크 권한을 완전히 거부할 수 있어요), 그 호출을 tools/call 요청으로 MCP 서버에 전달합니다.

실행 아키텍처 (Execution Architecture)

flowchart LR
    subgraph Host["MCP Host"]
        A[LLM] -->|writes code| B[Sandbox]
        B -->|function call| C[MCP Client]
        C -->|return value| B
        B -->|console output| A
    end
    C -->|tool call| D[MCP Server A]
    C -->|tool call| E[MCP Server B]
    D -->|result| C
    E -->|result| C

샌드박스는 모델이 생성한 코드를 직접 네트워크 접근이 없는 격리된 환경에서 실행합니다. 외부 세계와의 유일한 인터페이스는 호스트로 호출을 다시 라우팅하는 생성된 함수 스텁뿐입니다.

호스트는 브로커 역할을 합니다. 샌드박스에서 함수 호출을 받아 올바른 MCP 서버에 매핑하고, 도구 호출을 실행하고, 결과를 샌드박스로 돌려줍니다. 인증 토큰과 자격증명은 호스트가 보유하며 생성된 코드에 절대 노출되지 않습니다.

모델은 샌드박스가 반환하는 것, 즉 console.log 문의 출력이나 최종 반환 값만 봅니다. 이 덕분에 모델(그리고 클라이언트 개발자)이 컨텍스트 윈도우에 무엇이 들어갈지 정밀하게 제어할 수 있어요.

오류 처리 (Error Handling)

MCP 도구 오류는 전송 실패가 아니라 isError: true가 붙은 성공 응답으로 도착합니다. 생성된 래퍼는 이것을 던져진 예외(exception)로 변환해서, 모델이 작성한 코드가 try/catch를 사용할 수 있게 해야 합니다. 잡히지 않은 오류가 스크립트를 종료시키면, 그것을 스크립트의 결과로 드러내서 모델이 스스로 교정할 수 있게 하세요. 이미 커밋된 부분적인 부작용을 보고할 책임은 모델에게 있습니다.

두 패턴 결합하기 (Combining Both Patterns)

점진적 발견과 프로그래매틱 도구 호출은 서로 잘 어울립니다. 모델은 발견 도구를 사용해 필요한 도구를 식별하고, 그 스키마를 로드한 다음, 한 번의 실행으로 여러 도구를 호출하는 단일 스크립트를 작성합니다.

이 조합은 도구 정의의 토큰 비용과 도구 결과의 토큰 비용을 모두 최소화해서, 모델의 컨텍스트가 데이터를 통과시키는 일보다 추론에 집중하도록 해줍니다.