MCP 도구 검색

전체 MCP 카탈로그를 고정된 가상 도구 집합(mcp_tool_search, mcp_tool_call, agent_search, skill_search)으로 바꿔서, 수백 개의 도구를 가진 키도 tools/list에 네 개만 노출되게 해요. LLM이 키워드로 검색해 순위 매겨진 매치를 받은 후, 발견한 도구를 이름으로 호출해요. agent_searchA2A 에이전트 레지스트리에, skill_searchLiteLLM 호스팅 스킬 레지스트리에 대해 같은 일을 하며, 둘 다 키워드 대신 임베딩으로 순위를 매겨요.

관련 문서:

출처: 문서

본문

빠른 시작 (Quick start)

object_permission 아래 mcp_tool_search_enabled: true로 키를 생성하고, 검색이 볼 것이 있도록 mcp_servers(또는 mcp_access_groups)와 짝지은 다음 발견하고 호출하세요.

1. 도구 검색이 활성화된 키 생성:

curl -X POST http://localhost:4000/key/generate \
  -H "Authorization: Bearer ***" \
  -H "Content-Type: application/json" \
  -d '{
    "object_permission": {
      "mcp_tool_search_enabled": true,
      "mcp_servers": ["github", "slack"]
    }
  }'

2. tools/list가 가상 도구만 반환:

$ curl -s http://localhost:4000/mcp-rest/tools/list \
    -H "Authorization: Bearer ***" | jq '[.tools[].name]'
["mcp_tool_search", "mcp_tool_call", "agent_search", "skill_search"]

3. 검색이 실제 도구를 발견:

$ curl -s -X POST http://localhost:4000/mcp-rest/tools/call \
    -H "Authorization: Bearer ***" \
    -d '{"name":"mcp_tool_search","arguments":{"query":"add numbers"}}' \
  | jq -r '.content[0].text | fromjson | [.[].name]'
["math-add", "math-multiply"]

4. 발견한 도구 호출:

$ curl -s -X POST http://localhost:4000/mcp-rest/tools/call \
    -H "Authorization: Bearer ***" \
    -d '{"name":"mcp_tool_call","arguments":{"tool_name":"math-add","arguments":{"a":3,"b":4}}}' \
  | jq '{result: .content[0].text, isError}'
{
  "result": "7",
  "isError": false
}

같은 키는 실제 MCP 클라이언트용 streamable-http 프로토콜 엔드포인트(/mcp/)에서도 작동해요:

/mcp/에 대한 MCP Python SDK:

from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async with streamablehttp_client(
    "http://localhost:4000/mcp/",
    headers={"Authorization": f"Bearer {KEY}"},
) as (read, write, _):
    async with ClientSession(read, write) as session:
        await session.initialize()

        tools = await session.list_tools()
        print([t.name for t in tools.tools])
        # ['mcp_tool_search', 'mcp_tool_call', 'agent_search', 'skill_search']

        found = await session.call_tool("mcp_tool_search", {"query": "add numbers"})
        print(found.content[0].text)

        result = await session.call_tool(
            "mcp_tool_call",
            {"tool_name": "math-add", "arguments": {"a": 3, "b": 4}},
        )
        print(result.content[0].text)  # "7"

플래그가 없는 키는 기존 동작을 그대로 유지해요. tools/list는 전체 카탈로그를 반환하고, 두 가상 도구 이름은 forbidden 오류로 거부돼요.

모든 새 키의 기본값으로 활성화 (Enable as a default for every new key)

모든 새 키가 호출자가 플래그를 기억할 필요 없이 도구 검색에 옵트인되게 하려면 config.yamllitellm_settings.default_key_generate_params.object_permission 아래에 넣으세요. 필드를 생략한 /key/generate 요청에는 기본값이 병합되고, 부분 object_permission(예: mcp_servers만)을 설정한 요청은 명시적 필드를 유지한 채 미설정 필드만 가져와요.

config.yaml:

litellm_settings:
  default_key_generate_params:
    object_permission:
      mcp_tool_search_enabled: true
      mcp_servers: ["github", "slack"]

기본값은 호출자 스코프 검증이 실행된 후에 병합되므로, 일반 관리자가 아닌 개인 키 요청을 403으로 만들지 않아요. mcp_servers 같은 팀 스코프 필드는 호출자가 명시적으로 설정할 때 여전히 호출자 자신의 팀에 대해 검사되고, 관리자가 구성한 기본값은 검증 중인 요청이 아니라 유지된 키에만 적용돼요.

동작 방식 (How it works)

키의 object_permissionmcp_tool_search_enabled: true가 설정되면 streamable-http 엔드포인트(/mcp/)와 REST 표면(/mcp-rest/tools/list) 모두 키가 도달할 수 있는 MCP 서버 수와 무관하게 정확히 네 도구를 반환해요:

  • mcp_tool_search(query, top_k=5) — 쿼리와 일치하는 실제 도구의 순위 목록 반환.
  • mcp_tool_call(tool_name, arguments) — LLM이 검색을 통해 발견한 도구 중 하나를 실행.
  • agent_search(query, top_k=5) — 키가 도달할 수 있는 A2A 에이전트를 query가 설명하는 작업에 대한 시맨틱 유사도로 순위를 매겨 반환. litellm_settings.agent_search_embedding_model 필요(Search the registry 참고).
  • skill_search(query, top_k=5) — 키가 도달할 수 있는 LiteLLM 호스팅 스킬(custom_llm_provider=litellm_proxy)을 시맨틱 유사도로 순위를 매겨 반환. litellm_settings.skill_search_embedding_model 필요(Semantic Search over LiteLLM-Hosted Skills 참고).

두 핸들러 모두 일반 /tools/call 경로와 같은 필터링된 카탈로그와 디스패치 경로로 실행되므로, 검색은 키가 이미 볼 수 있는 도구만 표시하고 호출은 여전히 _get_allowed_mcp_serversexecute_mcp_tool로 해석돼요.

검색 알고리즘 (Search algorithm)

순위는 도구의 namedescription 필드에 대한 토큰 중복(token-overlap) 개수이며, 임베딩이나 추가 의존성이 없어요. 각 요청에서 프록시는:

  1. 쿼리를 소문자화하고 공백으로 토큰화("add numbers"["add", "numbers"]).
  2. 호출자가 도달할 수 있는 모든 도구에 대해 lower(name + " " + description)의 haystack 구축.
  3. haystack의 부분 문자열로 발견된 쿼리 토큰 수로 각 도구 점수 매김. addnumbers를 모두 포함하면 2점, add만 포함하면 1점.
  4. 점수 0은 버리고 나머지를 점수 내림차순으로 정렬한 후 처음 top_k(기본 5) 반환.

"점수 > 0" 이상의 유사도 임계값은 없으므로 한 토큰에만 걸리는 쿼리도 매치를 반환해요. 같은 점수의 도구 간 순서는 기본 카탈로그의 Python 안정 정렬을 따릅니다. 빈 쿼리는 빈 목록을 반환해요. mcp_tool_searchtop_k 인자는 호출별이므로, 첫 결과 집합이 너무 좁으면 LLM이 스스로 창을 넓힐 수 있어요.

사전 요구사항 (Prerequisites)

LiteLLM v1.92.x 이상 필요.

접근 제어 (Access control)

도구 검색은 접근 표면을 넓히지 않아요. mcp_tool_search는 일반 tools/list 핸들러가 사용하는 같은 필터링된 카탈로그를 순회하므로, 키가 도달할 수 없는 도구는 검색에 보이지 않아요. mcp_tool_call은 호출자의 허용 서버를 해석하고, 요청 IP 기반 filter_server_ids_by_ip 패스를 적용한 후 execute_mcp_tool로 디스패치하며, 이것이 서버 허용 목록과 호출자의 mcp_tool_permissions를 강제해요. 키 스코프 밖의 서버에 mcp_tool_call을 라우팅하려 하면 직접 호출을 보호하는 것과 같은 가드가 403을 반환해요:

$ curl -s -X POST http://localhost:4000/mcp-rest/tools/call \
    -H "Authorization: Bearer ***" \
    -d '{"name":"mcp_tool_call","arguments":{"tool_name":"secret-server-delete_all","arguments":{}}}'
{"detail":"User not allowed to call this tool. Allowed MCP servers: [math]"}

/key/info로 모든 키의 플래그를 검사할 수 있어요:

curl "http://localhost:4000/key/info?key=$KEY" \
  -H "Authorization: Bearer ***" \
  | jq '.info.object_permission | {mcp_tool_search_enabled, mcp_servers}'

도구 검색 vs. 시맨틱 필터: 언제 무엇을 쓰나요 (When to use tool search vs. semantic filter)

두 기능 모두 큰 카탈로그 폭발을 다루지만 서로 다른 레이어에 있어요. 도구 검색은 키당 옵트인되는 MCP 레이어로, LLM은 세 도구를 보고 MCP 프로토콜을 통해 스스로 발견을 구동하며, 종단 간 MCP를 말하는 에이전트 프레임워크에 적합해요. 시맨틱 필터/v1/responses/v1/chat/completions에 있고 각 요청에서 임베딩으로 도구 목록을 다시 써서, /mcp/를 직접 건드리지 않는 채팅 완성 호출자에 적합해요. 둘은 공존할 수 있습니다. 도구 검색이 켜진 키는 업스트림에서 시맨틱 필터링이 활성화되어 있어도 가상 도구 세 개만 노출해요.

더 알아보기 (Learn more)