본문으로 건너뛰기

[Gradio] MCP 도구 호출 레이턴시 개선 — HTTP 루프백 제거

PR 링크: gradio-app/gradio#12961 상태: Merged | 변경: +69 / -16

들어가며

Gradio의 MCP(Model Context Protocol) 서버에서 도구(tool) 호출 시, 기존에는 모든 요청이 gradio_client를 통해 HTTP 루프백(loopback)으로 처리되었다. 즉 같은 프로세스 내에서 HTTP 요청을 자기 자신에게 보내고, SSE로 결과를 수신하는 구조였다. queue=False로 설정된 간단한 이벤트에도 이 경로를 탔기 때문에 thread dispatch, TCP round-trip, SSE 파싱 등 불필요한 오버헤드가 발생했다.

핵심 코드 분석

call_tool 메서드의 분기 처리

기존에는 queued/non-queued 구분 없이 모든 호출이 client.submit()을 거쳤다. 변경 후에는 block_fn.queue 여부에 따라 fast path와 slow path를 명확히 분리한다.

Before:

async def call_tool(self, name, arguments):
    progress_token = None
    if self.mcp_server.request_context.meta is not None:
        progress_token = self.mcp_server.request_context.meta.progressToken

    client = await run_sync(self._get_or_create_client)
    endpoint_name, processed_args, request_headers, block_fn = (
        self._prepare_tool_call_args(name, arguments)
    )
    processed_args = self.insert_empty_state(block_fn.inputs, processed_args)
    job = client.submit(
        *processed_args, api_name=endpoint_name, headers=request_headers
    )

    if progress_token is None or not block_fn.queue:
        output_data = await self._execute_tool_without_progress(job)
    else:
        output_data = await self._execute_tool_with_progress(job, progress_token)

After:

async def call_tool(self, name, arguments):
    endpoint_name, processed_args, request_headers, block_fn = (
        self._prepare_tool_call_args(name, arguments)
    )
    processed_args = self.insert_empty_state(block_fn.inputs, processed_args)

    if not block_fn.queue:
        # Fast path: call blocks.process_api() directly
        session_state = SessionState(self.blocks)
        raw_output = await self.blocks.process_api(
            block_fn=block_fn,
            inputs=processed_args,
            state=session_state,
            request=self.mcp_server.request_context.request,
        )
        output_data = raw_output["data"]
    else:
        # Queued path: use HTTP loopback for streaming/progress
        progress_token = None
        if self.mcp_server.request_context.meta is not None:
            progress_token = self.mcp_server.request_context.meta.progressToken

        client = await run_sync(self._get_or_create_client)
        job = client.submit(
            *processed_args, api_name=endpoint_name, headers=request_headers,
        )
        if progress_token is None:
            output_data = await self._execute_tool_without_progress(job)
        else:
            output_data = await self._execute_tool_with_progress(job, progress_token)

핵심 변경: block_fn.queueFalse이면 client.submit() 대신 self.blocks.process_api()를 직접 호출한다. HTTP 클라이언트 생성, TCP 연결, SSE 파싱이 전부 생략된다.

SessionState 직접 생성

from gradio.state_holder import SessionState

session_state = SessionState(self.blocks)
raw_output = await self.blocks.process_api(
    block_fn=block_fn,
    inputs=processed_args,
    state=session_state,
    request=self.mcp_server.request_context.request,
)

HTTP 경로에서는 세션 관리가 미들웨어에서 자동 처리되지만, direct call에서는 SessionState를 수동으로 생성해야 한다.

왜 이게 좋은가

  • 레이턴시 감소: thread dispatch + TCP round-trip + SSE 오버헤드가 제거되어 MCP 도구 호출 응답 시간이 최대 10배 개선된다.
  • Queued 이벤트에 영향 없음: queue=True인 이벤트는 기존 HTTP 루프백 경로를 그대로 사용하여 progress notification, streaming 등의 기능이 보존된다.
  • 가이드 문서 업데이트: queue=False 설정의 성능 이점을 공식 문서에 명시하여 사용자가 선택할 수 있게 했다.

정리

MCP 통합에서 non-queued 이벤트의 불필요한 HTTP 루프백을 제거한 깔끔한 최적화다. 변경 코드량이 적고(+69/-16), queued/non-queued를 명확히 분리하여 기존 기능에 영향을 주지 않는다. MCP 도구 호출이 빈번한 환경에서 체감 성능 차이가 클 것으로 예상된다.

참고 자료

⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.

댓글

관련 포스트

PR Analysis 의 다른글