Главная / Статьи / Передача инструментов MCP через LiteLLM: пошаговое руководство по имитации банковского шлюза

Передача инструментов MCP через LiteLLM: пошаговое руководство по имитации банковского шлюза

Зарегистрируйте мок-банк с поддержкой стриминга по протоколу HTTP, сравните циклы работы ручно написанных инструментов с одним запросом к шлюзу и докажите, что механизмы защиты перед вызовом никогда не достигают MCP.

3709 слов

Фото от Quilia на Unsplash

Предполагается, что шлюз LiteLLM уже запущен (интерфейс администратора и база данных) с зарегистрированной моделью gpt-5.4-mini в Azure AI Foundry. В обзорной части рассматривается именно такая базовая настройка.

Что означает термин «агентный» в LiteLLM

Шлюз MCP от LiteLLM позволяет зарегистрированной модели обращаться к внешним инструментам через прокси. Шлюз находит необходимые инструменты, преобразует схемы и управляет процессом выполнения, благодаря чему код приложения не должен учитывать детали протокола MCP для каждого поставщика моделей. Ваш клиент взаимодействует с одним конечным пунктом вида OpenAI; шлюз находится между этим клиентом, моделью и любыми зарегистрированными серверами MCP.

Идентификаторы моделей легко ошибаться. В зависимости от способа добавления записи в Azure, LiteLLM может выдавать azure_ai/gpt-5.4-mini вместо простого gpt-5.4-mini. Сначала убедитесь в содержимом актуального реестра:

curl -X GET 'http://localhost:4000/v1/models' -H 'Authorization: Bearer sk-1234'
# Expected Results
{"data":[{"id":"azure_ai/gpt-5.4-mini","object":"model","created":1677610602,"owned_by":"openai"}],"object":"list"}

Используйте именно эту строку в каждом запросе. Несоответствие проявляется как Invalid model name passed in model=..., а не как неопределенная сетевая ошибка.

Один и тот же эндпоинт: обычный LLM или с инструментами

Отдельного URL для «режима MCP» не существует. /chat/completions и /v1/responses ведут себя одинаково как для обычного чата, так и для чата с использованием инструментов. Единственный фактор, влияющий на работу, — наличие в теле JSON массива tools (часто с указанием type: "mcp" для инструментов, управляемых шлюзом). Если пропустить tools, будет получен обычный результат, даже если шлюз подключен к серверам MCP. Если включить tools, шлюз может обнаружить их, вызвать, а затем включить полученные результаты в ответ на запрос.

Обычный запрос без использования инструментов выглядит так:

curl -X POST 'http://localhost:4000/chat/completions' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer sk-1234' \
  -d '{
    "model": "azure_ai/gpt-5.4-mini",
    "messages": [{"role": "user", "content": "What is the capital of France?"}]
  }'
# Expected Results
{"id":"chatcmpl-E778uLVTQJQLIJu0Dc2XAI1UYHuwL","created":1785364460,"model":"azure_ai/gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"The capital of France is **Paris**.","role":"assistant","provider_specific_fields":{"refusal":null},"annotations":[]},"provider_specific_fields":{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}}],"usage":{"completion_tokens":13,"prompt_tokens":13,"total_tokens":26,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"latency_checkpoint":{"engine_tbt_ms":8,"engine_ttft_ms":86,"engine_ttlt_ms":185,"pre_inference_ms":70,"service_tbt_ms":10,"service_ttft_ms":244,"service_ttlt_ms":354,"total_duration_ms":306,"user_visible_ttft_ms":174}},"service_tier":"default","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"jailbreak":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}]}

Частой причиной ошибок при копировании-вставке является наличие лишней кавычки перед именем модели (""azure_ai/gpt-5.4-mini"). Это приводит к ошибке Invalid JSON payload: unexpected character — это проблема синтаксиса тела запроса, а не сбой маршрута.

MCP Gateway: пошагово (проверено от начала до конца)

Мок-сервер банка

Вместо стандартного примера процесса MCP «hello-world» в этом руководстве используется небольшой мок-банк с тремя инструментами — get_balance, list_transactions и transfer_funds — которые работают с двумя аккаунтами в памяти. Реальные балансы и переводы позволяют точно определить, сработал ли полный маршрут. Каждый из приведенных ниже шагов был выполнен и проверен на основе этих данных.

Шаг 1 — запуск сервера (см. также отдельный файл mock_bank_server.py):

mkdir mock-bank-server && cd mock-bank-server
uv init .
uv add "mcp[cli]" openai
touch server.py

server.py использует симулированные данные, а не реальную книгу учета. Планируемая структура: клиент → LiteLLM → механизмы контроля → маршрутизатор → либо модель Azure, либо симулированный банковский процесс MCP вместе с тремя его инструментами.

После рассмотрения диаграммы важны некоторые детали поведения:

  • Интересный путь представляет собой цикл, а не одну стрелку. Модель запрашивает инструмент; шлюз направляет запрос к MCP; результат возвращается через шлюз; модель продолжает работу или снова запрашивает данные. Операции с одним инструментом автоматически завершают этот цикл; операции с несколькими инструментами могут выполняться частично автоматически только в той версии, которая протестирована здесь.
  • Крепления для ограждений располагаются в двух местах. Функция pre_call анализирует необработанный контент чата перед его направлением. Функция pre_mcp_call проверяет аргументы инструментов ближе к стороне MCP. На диаграммах часто показан один блок; на самом деле происходит перехват в разных этапах.
  • Пунктирная рамка «инструменты» представляет собой список внутри файла server.py, а не три отдельных микросервиса.
  • Симуляционный сервер MCP банка

    Примерно девяносто строк определяют реализацию FastMCP "mock-bank", включающую:

    • get_balance(account_id) — информация об владельце и остатке средств
    • list_transactions(account_id, limit) — последние операции
    • transfer_funds(from_account, to_account, amount) — перевод средств с базовой проверкой (неизвестный счет, недостаточно средств)
    from mcp.server.fastmcp import FastMCP
    from mcp.server.transport_security import TransportSecuritySettings
    
    mcp = FastMCP(
        "mock-bank",
        host="127.0.0.1",
        port=3001,
        transport_security=TransportSecuritySettings(
            allowed_hosts=["localhost:3001", "127.0.0.1:3001", "host.docker.internal:3001"],
        ),
    )
    
    # --- In-memory mock data (resets every restart) ---
    
    ACCOUNTS = {
        "ACC1001": {"owner": "Alice Johnson", "balance": 4250.75, "currency": "USD"},
        "ACC1002": {"owner": "Bob Smith", "balance": 980.10, "currency": "USD"},
    }
    
    TRANSACTIONS = {
        "ACC1001": [
            {"date": "2026-07-20", "description": "Grocery Store", "amount": -84.32},
            {"date": "2026-07-18", "description": "Payroll Deposit", "amount": 2500.00},
            {"date": "2026-07-15", "description": "Electric Bill", "amount": -120.44},
        ],
        "ACC1002": [
            {"date": "2026-07-21", "description": "Coffee Shop", "amount": -6.75},
            {"date": "2026-07-19", "description": "Freelance Payment", "amount": 450.00},
        ],
    }
    
    
    @mcp.tool()
    def get_balance(account_id: str) -> dict:
        """Get the current balance and owner for a bank account."""
        account = ACCOUNTS.get(account_id)
        if not account:
            return {"error": f"Account '{account_id}' not found"}
        return {
            "account_id": account_id,
            "owner": account["owner"],
            "balance": account["balance"],
            "currency": account["currency"],
        }
    
    
    @mcp.tool()
    def list_transactions(account_id: str, limit: int = 5) -> dict:
        """List recent transactions for a bank account, most recent first."""
        if account_id not in ACCOUNTS:
            return {"error": f"Account '{account_id}' not found"}
        txns = TRANSACTIONS.get(account_id, [])[:limit]
        return {"account_id": account_id, "transactions": txns}
    
    
    @mcp.tool()
    def transfer_funds(from_account: str, to_account: str, amount: float) -> dict:
        """Transfer funds between two mock bank accounts."""
        if from_account not in ACCOUNTS:
            return {"error": f"Source account '{from_account}' not found"}
        if to_account not in ACCOUNTS:
            return {"error": f"Destination account '{to_account}' not found"}
        if amount <= 0:
            return {"error": "Transfer amount must be positive"}
        if ACCOUNTS[from_account]["balance"] < amount:
            return {"error": f"Insufficient funds in '{from_account}'"}
    
        ACCOUNTS[from_account]["balance"] -= amount
        ACCOUNTS[to_account]["balance"] += amount
    
        return {
            "status": "success",
            "from_account": from_account,
            "to_account": to_account,
            "amount": amount,
            "new_balance_from": ACCOUNTS[from_account]["balance"],
            "new_balance_to": ACCOUNTS[to_account]["balance"],
        }
    
    
    if __name__ == "__main__":
        mcp.run(transport="streamable-http")
    

    Скрипты сравнения помогают понять, что заменяет шлюз:

    • test-direct-mcp.py взаимодействует только с сервером MCP — это позволяет проверить работоспособность инструментов без участия моделей.
    • test-direct-model.py напрямую обращается к Azure и MCP, а также вручную координирует процессы преобразования схемы, выполнения инструментов и второго этапа обработки; примерно семьдесят пять строк кода используются для того, чтобы шлюз преобразовал всё это в массив tools за один HTTP-запрос.
    • Ни один из скриптов не использует порт 4000; именно такое изоляционированное взаимодействие является основой сравнения «до» и «после».

    Запустите сервер и оставьте его работать:

    uv run python server.py
    
    # Execution results
    INFO:     Started server process [91854]
    INFO:     Waiting for application startup.
    [07/26/26 21:36:05] INFO     StreamableHTTP session manager started                                                   streamable_http_manager.py:131
    INFO:     Application startup complete.
    INFO:     Uvicorn running on http://127.0.0.1:3001 (Press CTRL+C to quit)
    [07/26/26 21:36:26] INFO     Created new transport with session ID: caf5347483ba4256977e29de8892a327
    

    Шаг 2 — два исправления, связанных с доступностью, прежде чем LiteLLM (в Docker) сможет взаимодействовать с хост-процессом

    Внутри контейнера Compose/Admin UI localhost относится к самому контейнеру, а не к вашему ноутбуку. Укажите URL MCP на DNS-адрес хоста Docker:

    http://host.docker.internal:3001/mcp
    

    Кроме того, механизм защиты от перенаправления DNS в SDK MCP может возвращать ответ 421 Misdirected Request, если хост, через который происходит пересылка запроса, не включен в список разрешенных сервером:

    from mcp.server.fastmcp import FastMCP
    from mcp.server.transport_security import TransportSecuritySettings
    
    mcp = FastMCP(
        "mock-bank",
        host="127.0.0.1",
        port=3001,
        transport_security=TransportSecuritySettings(
            allowed_hosts=["localhost:3001", "127.0.0.1:3001", "host.docker.internal:3001"],
        ),
    )
    

    Шаг 3 — регистрация сервера в LiteLLM

    В интерфейсе: MCP Servers → Add MCP Server

    • Имя: mock_bank
    • Транспорт: Streamable HTTP (должно совпадать с mcp.run(transport="streamable-http"))
    • URL: http://host.docker.internal:3001/mcp
    • Аутентификация: не требуется для этой локальной демонстрации

    Или через конфигурацию:

    mcp_servers:
      mock_bank:
        url: http://host.docker.internal:3001/mcp
        transport: streamable_http
        auth_type: none
    

    Проверьте, что статус подключения показывает Connection Status: Connected, и что в разделе Tool Configuration отображаются все три включенных инструмента.

    Шаг 4 — проверка строки с именем модели, которая фактически зарегистрирована шлюзом

    curl -X GET 'http://localhost:4000/v1/models' -H 'Authorization: Bearer sk-1234'
    # Expected Result
    {"data":[{"id":"azure_ai/gpt-5.4-mini","object":"model","created":1677610602,"owned_by":"openai"}],"object":"list"}
    

    Несмотря на префикс azure_ai/, трафик всё равно попадал в тестовую систему чата Azure OpenAI (в ответах появлялись поля фильтрации контента). Рассматривайте этот идентификатор как особенность названия, которую следует копировать дословно, а не как ошибку поставщика услуг.

    Шаг 5 — один инструмент за раз (чистый путь)

    curl --location 'http://localhost:4000/v1/responses' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer sk-1234" \
      --data '{
        "model": "azure_ai/gpt-5.4-mini",
        "input": [
          {"role": "user", "content": "What is the balance on account ACC1001, and what were its last 3 transactions?", "type": "message"}
        ],
        "tools": [
          {
            "type": "mcp",
            "server_label": "mock_bank",
            "server_url": "litellm_proxy",
            "require_approval": "never"
          }
        ],
        "tool_choice": "required"
      }'
    
    # Expected Result
    {"id":"resp_QQRbArl1kNuzmPJahCQIWJvpgxSgsw7SuuwX_pZJfuWmrGiguydiPvF7EhPhfRrojCr6UtQGUTlWEA3FSDBl4c1PSDpccSmbIZWgCMizU1Vp8gDt_jokT8ZOL9tIiYK0pUHjYVGu3PgbJjz5J0jkAJUFKshh_-9xn0zVGf08-33WD19mcLCh3qfxRM4AcMciWE4nNQa_TrS-6U6olfonnXvge9qszI4u6dA02Eo6GlLmEpGE19InxbopOLBny1QNdMDgdC4SzzZh9XWnQvFNu-IjYEe68pfoJ5u0ohZE1scbgF9ABNMAaN94vfGIuApDd9ibCBpgbHbcTsrE86EGP6_XJ_72uwG_aTHTjgZ-laFUxCIjEgEEo8S09Ojq-eupjWI8WTwSOT_ozMKVF0cyOuQKwkpARIrIeGE=","created_at":1785242019,"error":null,"incomplete_details":null,"instructions":null,"metadata":{},"model":"azure_ai/gpt-5.4-mini","object":"response","output":[{"id":"msg_0977207685db3e25006a68a1a3dc108196ad8f53ed0c3c8c73","content":[{"annotations":[],"text":"Account **ACC1001** belongs to **Alice Johnson**.\n\n- **Balance:** **$3,450.75 USD**\n\nLast 3 transactions:\n1. **2026-07-20** — Grocery Store — **-$84.32**\n2. **2026-07-18** — Payroll Deposit — **+$2,500.00**\n3. **2026-07-15** — Electric Bill — **-$120.44**","type":"output_text","logprobs":[]}],"role":"assistant","status":"completed","type":"message","phase":"final_answer"},{"type":"mcp_tools_fetched","id":"mcp_tools_cbeff22f","status":"completed","role":"system","content":[{"type":"output_text","text":"[\n  \"name='mock_bank-get_balance' title=None description='Get the current balance and owner for a bank account.' inputSchema={'properties': {'account_id': {'title': 'Account Id', 'type': 'string'}}, 'required': ['account_id'], 'title': 'get_balanceArguments', 'type': 'object'} outputSchema=None icons=None annotations=None meta=None execution=None\",\n  \"name='mock_bank-list_transactions' title=None description='List recent transactions for a bank account, most recent first.' inputSchema={'properties': {'account_id': {'title': 'Account Id', 'type': 'string'}, 'limit': {'default': 5, 'title': 'Limit', 'type': 'integer'}}, 'required': ['account_id'], 'title': 'list_transactionsArguments', 'type': 'object'} outputSchema=None icons=None annotations=None meta=None execution=None\",\n  \"name='mock_bank-transfer_funds' title=None description='Transfer funds between two mock bank accounts.' inputSchema={'properties': {'from_account': {'title': 'From Account', 'type': 'string'}, 'to_account': {'title': 'To Account', 'type': 'string'}, 'amount': {'title': 'Amount', 'type': 'number'}}, 'required': ['from_account', 'to_account', 'amount'], 'title': 'transfer_fundsArguments', 'type': 'object'} outputSchema=None icons=None annotations=None meta=None execution=None\"\n]","annotations":[]}],"phase":null},{"type":"tool_execution_results","id":"tool_results_ee9f6c7e","status":"completed","role":"system","content":[{"type":"output_text","text":"[\n  {\n    \"tool_call_id\": \"call_ToKnEWo532C8nDABKElHCSBs\",\n    \"result\": \"{\\n  \\\"account_id\\\": \\\"ACC1001\\\",\\n  \\\"owner\\\": \\\"Alice Johnson\\\",\\n  \\\"balance\\\": 3450.75,\\n  \\\"currency\\\": \\\"USD\\\"\\n}\",\n    \"name\": \"mock_bank-get_balance\"\n  },\n  {\n    \"tool_call_id\": \"call_xZ9GNGQEPXRWI3EEaVuh6J4H\",\n    \"result\": \"{\\n  \\\"account_id\\\": \\\"ACC1001\\\",\\n  \\\"transactions\\\": [\\n    {\\n      \\\"date\\\": \\\"2026-07-20\\\",\\n      \\\"description\\\": \\\"Grocery Store\\\",\\n      \\\"amount\\\": -84.32\\n    },\\n    {\\n      \\\"date\\\": \\\"2026-07-18\\\",\\n      \\\"description\\\": \\\"Payroll Deposit\\\",\\n      \\\"amount\\\": 2500.0\\n    },\\n    {\\n      \\\"date\\\": \\\"2026-07-15\\\",\\n      \\\"description\\\": \\\"Electric Bill\\\",\\n      \\\"amount\\\": -120.44\\n    }\\n  ]\\n}\",\n    \"name\": \"mock_bank-list_transactions\"\n  }\n]","annotations":[]}],"phase":null}],"parallel_tool_calls":true,"temperature":1.0,"tool_choice":"auto","tools":[{"name":"mock_bank-get_balance","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"}},"required":["account_id"],"title":"get_balanceArguments","type":"object","additionalProperties":false},"strict":false,"type":"function","defer_loading":null,"description":"Get the current balance and owner for a bank account."},{"name":"mock_bank-list_transactions","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"},"limit":{"default":5,"title":"Limit","type":"integer"}},"required":["account_id"],"title":"list_transactionsArguments","type":"object","additionalProperties":false},"strict":false,"type":"function","defer_loading":null,"description":"List recent transactions for a bank account, most recent first."},{"name":"mock_bank-transfer_funds","parameters":{"properties":{"from_account":{"title":"From Account","type":"string"},"to_account":{"title":"To Account","type":"string"},"amount":{"title":"Amount","type":"number"}},"required":["from_account","to_account","amount"],"title":"transfer_fundsArguments","type":"object","additionalProperties":false},"strict":false,"type":"function","defer_loading":null,"description":"Transfer funds between two mock bank accounts."}],"top_p":0.98,"max_output_tokens":null,"previous_response_id":"resp_0977207685db3e25006a68a1a1f48c8196b7990872db041d14","reasoning":{"context":"current_turn","effort":"none","mode":"standard","summary":null},"status":"completed","text":{"format":{"type":"text"},"verbosity":"medium"},"truncation":"disabled","usage":{"input_tokens":473,"input_tokens_details":{"audio_tokens":null,"cached_tokens":0,"text_tokens":null},"output_tokens":97,"output_tokens_details":{"reasoning_tokens":0,"text_tokens":null},"total_tokens":570,"cost":null},"user":null,"store":true,"background":false,"completed_at":1785242020,"content_filters":[{"blocked":false,"source_type":"completion","content_filter_raw":[],"content_filter_results":{"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"hate":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"}},"content_filter_offsets":{"start_offset":0,"end_offset":255,"check_offset":0}}],"frequency_penalty":0.0,"max_tool_calls":null,"moderation":null,"presence_penalty":0.0,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","safety_identifier":null,"service_tier":"default","top_logprobs":0
    

    При успешном запуске модель вызывала функции get_balance и list_transactions, затем отвечала с данными из имитатора — Элис Джонсон на ACC1001, баланс, совпадающий с данными в памяти, и три последние операции из файла mock_bank_server.py. Это подтверждает последовательность: шлюз → поиск → выполнение → окончательный ответ.

    Шаг 6 — вызов, изменяющий состояние, когда это единственный инструмент в текущей операции

    curl -X POST 'http://localhost:4000/chat/completions' \
      -H 'Content-Type: application/json' \
      -H 'Authorization: Bearer sk-1234' \
      -d '{
        "model": "azure_ai/gpt-5.4-mini",
        "messages": [{"role": "user", "content": "First, transfer $200 from ACC1001 to ACC1002. Do not call any other tools yet."}],
        "tools": [{"type": "mcp", "server_label": "mock_bank", "server_url": "litellm_proxy", "require_approval": "never"}],
        "tool_choice": "auto"
      }'
    # Expected result
    {"id":"chatcmpl-E6bKV9nPurWICiuBlkgwaJzOS8vIC","created":1785242171,"model":"azure_ai/gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"Done — transferred $200 from ACC1001 to ACC1002 successfully.","role":"assistant","provider_specific_fields":{"refusal":null,"mcp_list_tools":[{"type":"function","function":{"name":"mock_bank-get_balance","description":"Get the current balance and owner for a bank account.","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"}},"required":["account_id"],"title":"get_balanceArguments","type":"object","additionalProperties":false},"strict":false}},{"type":"function","function":{"name":"mock_bank-list_transactions","description":"List recent transactions for a bank account, most recent first.","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"},"limit":{"default":5,"title":"Limit","type":"integer"}},"required":["account_id"],"title":"list_transactionsArguments","type":"object","additionalProperties":false},"strict":false}},{"type":"function","function":{"name":"mock_bank-transfer_funds","description":"Transfer funds between two mock bank accounts.","parameters":{"properties":{"from_account":{"title":"From Account","type":"string"},"to_account":{"title":"To Account","type":"string"},"amount":{"title":"Amount","type":"number"}},"required":["from_account","to_account","amount"],"title":"transfer_fundsArguments","type":"object","additionalProperties":false},"strict":false}}],"mcp_tool_calls":[{"function":{"arguments":"{\"from_account\":\"ACC1001\",\"to_account\":\"ACC1002\",\"amount\":200}","name":"mock_bank-transfer_funds"},"id":"call_PIN38jt2KT9M4hkoxEKXXXaD","type":"function"}],"mcp_call_results":[{"tool_call_id":"call_PIN38jt2KT9M4hkoxEKXXXaD","result":"{\n  \"status\": \"success\",\n  \"from_account\": \"ACC1001\",\n  \"to_account\": \"ACC1002\",\n  \"amount\": 200.0,\n  \"new_balance_from\": 3250.75,\n  \"new_balance_to\": 1980.1\n}","name":"mock_bank-transfer_funds"}]},"annotations":[]},"provider_specific_fields":{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}}],"usage":{"completion_tokens":19,"prompt_tokens":373,"total_tokens":392,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"latency_checkpoint":{"engine_tbt_ms":5,"engine_ttft_ms":30,"engine_ttlt_ms":116,"pre_inference_ms":120,"service_tbt_ms":5,"service_ttft_ms":306,"service_ttlt_ms":316,"total_duration_ms":223,"user_visible_ttft_ms":186}},"service_tier":"default","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{}}]}
    

    Результат: finish_reason: „stop“, автоматически выполнена операция transfer_funds, приведен итоговый отчет с обновленными балансами. Перевод денег — именно тот тип операции, который требует ограничений перед тем, как он будет выполнен в демо-версии программы.

    Урок — одновременные вызовы инструментов в одном шаге

    Сложные запросы вроде «перевести $200 с ACC1001 на ACC1002, затем подтвердить балансы обоих счетов» приводили к тому, что модель выполняла три вызова инструментов одновременно. В тестируемой версии LiteLLM автоматическое выполнение MCP надежно завершало лишь один вызов (transfer_funds, отображаемый в mcp_tool_calls / mcp_call_results), в то время как оставшиеся записи get_balance оставались нерешенными в массиве tool_calls формата OpenAI — без какого-либо итогового текста, поскольку модель все еще ждала. Такое поведение наблюдалось как в разделе /v1/responses, так и в разделе /chat/completions, при этом параметр tool_choice мог быть установлен как в значение "required", так и "auto". Следовательно, это не особенность формата запроса; это способ, которым данная версия обрабатывала множественные параллельные вызовы инструментов MCP в одном ходе.

    Практические рекомендации по этой задаче: запрашивайте один инструмент за раз (как в шаге 6). Если вам действительно нужен параллелизм нескольких вызовов, выполните цикл самостоятельно — запустите оставшиеся tool_calls и отправьте результаты с параметром role: "tool", то есть используйте ту же схему, что показана в скриптах Direct и Gateway ниже. Выбор модели по-прежнему влияет на надежность агента; «идеальный» выбор вчера может стать устаревшим к следующему циклу обновлений, поэтому сохраняйте возможность замены механизма оркестрации.

    Шаг 7 — проверка процесса обнаружения без участия модели

    curl -X POST 'http://localhost:4000/mcp/mock_bank' \
      -H 'Authorization: Bearer sk-1234' \
      -H 'Content-Type: application/json' \
      -H 'Accept: application/json, text/event-stream' \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
    
    # Expected result
    event: message
    data: {"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"mock_bank-get_balance","description":"Get the current balance and owner for a bank account.","inputSchema":{"properties":{"account_id":{"title":"Account Id","type":"string"}},"required":["account_id"],"title":"get_balanceArguments","type":"object"}},{"name":"mock_bank-list_transactions","description":"List recent transactions for a bank account, most recent first.","inputSchema":{"properties":{"account_id":{"title":"Account Id","type":"string"},"limit":{"default":5,"title":"Limit","type":"integer"}},"required":["account_id"],"title":"list_transactionsArguments","type":"object"}},{"name":"mock_bank-transfer_funds","description":"Transfer funds between two mock bank accounts.","inputSchema":{"properties":{"from_account":{"title":"From Account","type":"string"},"to_account":{"title":"To Account","type":"string"},"amount":{"title":"Amount","type":"number"}},"required":["from_account","to_account","amount"],"title":"transfer_fundsArguments","type":"object"}}]}}
    

    Этот вызов позволяет отличить случаи, когда процесс обнаружения не работает, от ситуаций, когда модель отказалась вызывать инструмент или была выполнена только часть операции.

    Direct против Gateway: что дает прокси

    Тест 1 — чистый MCP, без модели и прокси. Проверьте работу банковского сервера в одиночку:

    import asyncio
    from mcp.client.streamable_http import streamablehttp_client
    from mcp.client.session import ClientSession
    
    
    async def main():
        async with streamablehttp_client("http://127.0.0.1:3001/mcp") as (read, write, _):
            async with ClientSession(read, write) as session:
                await session.initialize()
    
                # 1. Discover what tools exist
                tools = await session.list_tools()
                print("Available tools:", [t.name for t in tools.tools])
    
                # 2. Call a tool directly — YOU decide which tool and args.
                #    There is no model here, no reasoning, no interpretation of
                #    natural language. You must already know exactly what to call.
                result = await session.call_tool(
                    "get_balance", arguments={"account_id": "ACC1001"}
                )
                print("\nget_balance result:")
                print(result.content[0].text)
    
                result2 = await session.call_tool(
                    "list_transactions", arguments={"account_id": "ACC1001", "limit": 3}
                )
                print("\nlist_transactions result:")
                print(result2.content[0].text)
    
    
    if __name__ == "__main__":
        asyncio.run(main())
    

    Ожидаемый вывод списка и баланса:

    uv run test-direct-mcp.py
    # Expected result
    Available tools: ['get_balance', 'list_transactions', 'transfer_funds']
    
    get_balance result:
    {
      "account_id": "ACC1001",
      "owner": "Alice Johnson",
      "balance": 3250.75,
      "currency": "USD"
    }
    
    list_transactions result:
    {
      "account_id": "ACC1001",
      "transactions": [
        {
          "date": "2026-07-20",
          "description": "Grocery Store",
          "amount": -84.32
        },
        {
          "date": "2026-07-18",
          "description": "Payroll Deposit",
          "amount": 2500.0
        },
        {
          "date": "2026-07-15",
          "description": "Electric Bill",
          "amount": -120.44
        }
      ]
    }
    

    Тест 2 — прямая модель плюс ручная оркестрация. Получение инструментов MCP, преобразование схем в формат инструментов OpenAI, вызов Azure, выполнение инструментов, возврат результатов — вашему приложению необходимо написать примерно семьдесят пять строк, без каких-либо общих ограничений или отслеживания расходов:

    import asyncio
    import json
    import os
    
    from openai import AzureOpenAI
    from mcp.client.streamable_http import streamablehttp_client
    from mcp.client.session import ClientSession
    
    MODEL_DEPLOYMENT = "gpt-5.4-mini"
    
    
    def mcp_tool_to_openai_format(mcp_tool):
        """Manual translation step #2 — the gateway normally does this for you."""
        return {
            "type": "function",
            "function": {
                "name": mcp_tool.name,
                "description": mcp_tool.description or "",
                "parameters": mcp_tool.inputSchema,
            },
        }
    
    
    async def main():
        client = AzureOpenAI(
            api_key=os.environ["AZURE_API_KEY"],
            api_version="2024-10-21",
            azure_endpoint=os.environ["AZURE_API_BASE"],
        )
    
        async with streamablehttp_client("http://127.0.0.1:3001/mcp") as (read, write, _):
            async with ClientSession(read, write) as session:
                await session.initialize()
    
                # Step 1: discover tools yourself
                mcp_tools = (await session.list_tools()).tools
                openai_tools = [mcp_tool_to_openai_format(t) for t in mcp_tools]
    
                messages = [
                    {
                        "role": "user",
                        "content": "What is the balance on account ACC1001, and "
                        "what were its last 3 transactions?",
                    }
                ]
    
                # Step 3: call the model directly
                response = client.chat.completions.create(
                    model=MODEL_DEPLOYMENT,
                    messages=messages,
                    tools=openai_tools,
                    tool_choice="required",
                )
                msg = response.choices[0].message
                messages.append(msg.model_dump(exclude_none=True))
    
                # Step 4: manually execute any tool calls the model requested
                for tool_call in msg.tool_calls or []:
                    args = json.loads(tool_call.function.arguments)
                    result = await session.call_tool(tool_call.function.name, arguments=args)
                    messages.append(
                        {
                            "role": "tool",
                            "tool_call_id": tool_call.id,
                            "content": result.content[0].text,
                        }
                    )
    
                # Second turn: give the model the tool results, get final answer
                final = client.chat.completions.create(
                    model=MODEL_DEPLOYMENT,
                    messages=messages,
                )
                print(final.choices[0].message.content)
    
    
    if __name__ == "__main__":
        asyncio.run(main())
    

    Пример успешного ответа в формате повествования:

    uv run test-direct-model.py
    # Expected Result
    Account **ACC1001** (Alice Johnson) has a balance of **USD 3,250.75**.
    
    Last 3 transactions:
    1. **2026-07-20** — Grocery Store — **-84.32**
    2. **2026-07-18** — Payroll Deposit — **+2,500.00**
    3. **2026-07-15** — Electric Bill — **-120.44**
    

    Тест 3 — та же цель с использованием LiteLLM. Один HTTP-запрос; процессы обнаружения, преобразования, выполнения и последующего ответа происходят непосредственно в шлюзе:

    curl --location 'http://localhost:4000/v1/responses' \
      --header 'Content-Type: application/json' \
      --header "Authorization: Bearer sk-1234" \
      --data '{
        "model": "azure_ai/gpt-5.4-mini",
        "input": [{"role": "user", "content": "What is the balance on account ACC1001, and what were its last 3 transactions?", "type": "message"}],
        "tools": [{"type": "mcp", "server_label": "mock_bank", "server_url": "litellm_proxy", "require_approval": "never"}],
        "tool_choice": "required"
      }'
    
    # Expected Result
    {"id":"resp_DCjguhF87Zw3ekO8Jw3782IkYnZQfG_avgYD_kkI-NmeDU9bNSDRFM3FTGJl31tMzutajvYJ6x_S8siU06zyXozMCPC1pSQhXraDtpREE_RRXq23BZDJR37fxmPqzVsdhlk-o2IEF3C8Z3251nJPIecUJM75GFAYdXygfMfoqmM1Jdaxc3VQfcZKMaW1IOG7528zrRqms58F5xwpR0vvFScwclLhOYrch5WQRY6KBBi-60wMBuylfQNfqT8cWwvbeG7Xbe6OKEiBXFzWOx7QkaGhckDFat-DZ3RFW8Iv8SP0a21rA_9h19QUijB-sPBaD9BNIKk4I2dRqz2B1vMtyut3GpX8mO5D5dinKruIMy2nrMatBun8uJC5Mt76hWgFc6YzH3h_ihsZjCRzWz1j85VowrVdnOYGGhg=","created_at":1785242960,"error":null,"incomplete_details":null,"instructions":null,"metadata":{},"model":"azure_ai/gpt-5.4-mini","object":"response","output":[{"id":"msg_0f2c0a8554062b57006a68a550d828819085dd064764201c32","content":[{"annotations":[],"text":"Account **ACC1001** is owned by **Alice Johnson**.\n\n- **Balance:** **$3,250.75 USD**\n\n**Last 3 transactions:**\n1. **2026-07-20** — Grocery Store — **-$84.32**\n2. **2026-07-18** — Payroll Deposit — **+$2,500.00**\n3. **2026-07-15** — Electric Bill — **-$120.44**","type":"output_text","logprobs":[]}],"role":"assistant","status":"completed","type":"message","phase":"final_answer"},{"type":"mcp_tools_fetched","id":"mcp_tools_9d9b1cdc","status":"completed","role":"system","content":[{"type":"output_text","text":"[\n  \"name='mock_bank-get_balance' title=None description='Get the current balance and owner for a bank account.' inputSchema={'properties': {'account_id': {'title': 'Account Id', 'type': 'string'}}, 'required': ['account_id'], 'title': 'get_balanceArguments', 'type': 'object'} outputSchema=None icons=None annotations=None meta=None execution=None\",\n  \"name='mock_bank-list_transactions' title=None description='List recent transactions for a bank account, most recent first.' inputSchema={'properties': {'account_id': {'title': 'Account Id', 'type': 'string'}, 'limit': {'default': 5, 'title': 'Limit', 'type': 'integer'}}, 'required': ['account_id'], 'title': 'list_transactionsArguments', 'type': 'object'} outputSchema=None icons=None annotations=None meta=None execution=None\",\n  \"name='mock_bank-transfer_funds' title=None description='Transfer funds between two mock bank accounts.' inputSchema={'properties': {'from_account': {'title': 'From Account', 'type': 'string'}, 'to_account': {'title': 'To Account', 'type': 'string'}, 'amount': {'title': 'Amount', 'type': 'number'}}, 'required': ['from_account', 'to_account', 'amount'], 'title': 'transfer_fundsArguments', 'type': 'object'} outputSchema=None icons=None annotations=None meta=None execution=None\"\n]","annotations":[]}],"phase":null},{"type":"tool_execution_results","id":"tool_results_56298345","status":"completed","role":"system","content":[{"type":"output_text","text":"[\n  {\n    \"tool_call_id\": \"call_t6y64QgQjDT4jGMLOEyFxc5u\",\n    \"result\": \"{\\n  \\\"account_id\\\": \\\"ACC1001\\\",\\n  \\\"owner\\\": \\\"Alice Johnson\\\",\\n  \\\"balance\\\": 3250.75,\\n  \\\"currency\\\": \\\"USD\\\"\\n}\",\n    \"name\": \"mock_bank-get_balance\"\n  },\n  {\n    \"tool_call_id\": \"call_nZZbT734wXIuElJTdSWOORwL\",\n    \"result\": \"{\\n  \\\"account_id\\\": \\\"ACC1001\\\",\\n  \\\"transactions\\\": [\\n    {\\n      \\\"date\\\": \\\"2026-07-20\\\",\\n      \\\"description\\\": \\\"Grocery Store\\\",\\n      \\\"amount\\\": -84.32\\n    },\\n    {\\n      \\\"date\\\": \\\"2026-07-18\\\",\\n      \\\"description\\\": \\\"Payroll Deposit\\\",\\n      \\\"amount\\\": 2500.0\\n    },\\n    {\\n      \\\"date\\\": \\\"2026-07-15\\\",\\n      \\\"description\\\": \\\"Electric Bill\\\",\\n      \\\"amount\\\": -120.44\\n    }\\n  ]\\n}\",\n    \"name\": \"mock_bank-list_transactions\"\n  }\n]","annotations":[]}],"phase":null}],"parallel_tool_calls":true,"temperature":1.0,"tool_choice":"auto","tools":[{"name":"mock_bank-get_balance","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"}},"required":["account_id"],"title":"get_balanceArguments","type":"object","additionalProperties":false},"strict":false,"type":"function","defer_loading":null,"description":"Get the current balance and owner for a bank account."},{"name":"mock_bank-list_transactions","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"},"limit":{"default":5,"title":"Limit","type":"integer"}},"required":["account_id"],"title":"list_transactionsArguments","type":"object","additionalProperties":false},"strict":false,"type":"function","defer_loading":null,"description":"List recent transactions for a bank account, most recent first."},{"name":"mock_bank-transfer_funds","parameters":{"properties":{"from_account":{"title":"From Account","type":"string"},"to_account":{"title":"To Account","type":"string"},"amount":{"title":"Amount","type":"number"}},"required":["from_account","to_account","amount"],"title":"transfer_fundsArguments","type":"object","additionalProperties":false},"strict":false,"type":"function","defer_loading":null,"description":"Transfer funds between two mock bank accounts."}],"top_p":0.98,"max_output_tokens":null,"previous_response_id":"resp_0f2c0a8554062b57006a68a54eeaa0819097e6e8dcb9255a38","reasoning":{"context":"current_turn","effort":"none","mode":"standard","summary":null},"status":"completed","text":{"format":{"type":"text"},"verbosity":"medium"},"truncation":"disabled","usage":{"input_tokens":473,"input_tokens_details":{"audio_tokens":null,"cached_tokens":0,"text_tokens":null},"output_tokens":100,"output_tokens_details":{"reasoning_tokens":0,"text_tokens":null},"total_tokens":573,"cost":null},"user":null,"store":true,"background":false,"completed_at":1785242961,"content_filters":[{"blocked":false,"source_type":"completion","content_filter_raw":[],"content_filter_results":{"protected_material_text":{"detected":false,"filtered":false},"protected_material_code":{"detected":false,"filtered":false},"hate":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"}},"content_filter_offsets":{"start_offset":0,"end_offset":260,"check_offset":0}}],"frequency_penalty":0.0,"max_tool_calls":null,"moderation":null,"presence_penalty":0.0,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","safety_identifier":null,"service_tier":"default","top_logprobs":0}
    
    Проблема Прямой способ (Тест 2) Шлюз (Тест 3)
    Код приложения ~75 строк на приложение Один HTTP-запрос
    Преобразование схем Вручную для каждого сервера MCP Автоматически
    Цикл использования инструментов Вы сами его поддерживаете
    Внутренний Замена поставщика Переписывание вызовов SDK Изменение строки model Аутентификация на нескольких серверах MCP Индивидуальные настройки для каждого сервера Централизованная конфигурация mcp_servers Ограничения/контроль расходов Разработка собственных решений Доступно при настройке

    Ограничения, такие как маскировка адресов электронной почты, блокировка номеров карт и ключевых слов, выполняются в фазе pre_call — **до** обнаружения или выполнения запросов MCP. Заблокированный запрос никогда не должен достигать модели банка. Именно это свойство нужно подтверждать, а не только то, что текст выглядит замаскированным в ответе.

    curl -X POST 'http://localhost:4000/chat/completions' \
      -H 'Content-Type: application/json' \
      -H 'Authorization: Bearer sk-1234' \
      -d '{
        "model": "azure_ai/gpt-5.4-mini",
        "messages": [{"role": "user", "content": "Check the balance on ACC1001 and email the results to john.doe@example.com"}],
        "tools": [{"type": "mcp", "server_label": "mock_bank", "server_url": "litellm_proxy", "require_approval": "never"}],
        "tool_choice": "auto",
        "guardrails": ["basic-content-filter"]
      }'
    # Expected result
    {"id":"chatcmpl-E6xkAAMouHdsDuJR038UZH4Thej9z","created":1785328330,"model":"azure_ai/gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"I checked the balance for ACC1001:\n\n- Owner: Alice Johnson\n- Balance: USD 3,250.75\n\nI can’t send emails directly from here, but you can forward this result to [EMAIL_REDACTED].","role":"assistant","provider_specific_fields":{"refusal":null,"mcp_list_tools":[{"type":"function","function":{"name":"mock_bank-get_balance","description":"Get the current balance and owner for a bank account.","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"}},"required":["account_id"],"title":"get_balanceArguments","type":"object","additionalProperties":false},"strict":false}},{"type":"function","function":{"name":"mock_bank-list_transactions","description":"List recent transactions for a bank account, most recent first.","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"},"limit":{"default":5,"title":"Limit","type":"integer"}},"required":["account_id"],"title":"list_transactionsArguments","type":"object","additionalProperties":false},"strict":false}},{"type":"function","function":{"name":"mock_bank-transfer_funds","description":"Transfer funds between two mock bank accounts.","parameters":{"properties":{"from_account":{"title":"From Account","type":"string"},"to_account":{"title":"To Account","type":"string"},"amount":{"title":"Amount","type":"number"}},"required":["from_account","to_account","amount"],"title":"transfer_fundsArguments","type":"object","additionalProperties":false},"strict":false}}],"mcp_tool_calls":[{"function":{"arguments":"{\"account_id\":\"ACC1001\"}","name":"mock_bank-get_balance"},"id":"call_4jiRhEZhMUSY8GLWJHfDWulI","type":"function"}],"mcp_call_results":[{"tool_call_id":"call_4jiRhEZhMUSY8GLWJHfDWulI","result":"{\n  \"account_id\": \"ACC1001\",\n  \"owner\": \"Alice Johnson\",\n  \"balance\": 3250.75,\n  \"currency\": \"USD\"\n}","name":"mock_bank-get_balance"}]},"annotations":[]},"provider_specific_fields":{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}}],"usage":{"completion_tokens":53,"prompt_tokens":332,"total_tokens":385,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"latency_checkpoint":{"engine_tbt_ms":4,"engine_ttft_ms":36,"engine_ttlt_ms":260,"pre_inference_ms":125,"service_tbt_ms":4,"service_ttft_ms":375,"service_ttlt_ms":594,"total_duration_ms":478,"user_visible_ttft_ms":250}},"service_tier":"default","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{}}]}
    

    Повторите без указания названий ограничений в тексте пользователя:

    curl -X POST 'http://localhost:4000/chat/completions' \
      -H 'Content-Type: application/json' \
      -H 'Authorization: Bearer sk-1234' \
      -d '{
        "model": "azure_ai/gpt-5.4-mini",
        "messages": [{"role": "user", "content": "Check the balance on ACC1001 and email the results to john.doe@example.com"}],
        "tools": [{"type": "mcp", "server_label": "mock_bank", "server_url": "litellm_proxy", "require_approval": "never"}],
        "tool_choice": "auto"
      }'
    
    # Expected result
    {"id":"chatcmpl-E6xlbLbaUP1awks92ZGSrsHCB8sl0","created":1785328419,"model":"azure_ai/gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"stop","index":0,"message":{"content":"I checked ACC1001:\n\n- Owner: Alice Johnson\n- Balance: $3,250.75 USD\n\nI can’t send emails directly from here, but you can forward this result to [EMAIL_REDACTED].","role":"assistant","provider_specific_fields":{"refusal":null,"mcp_list_tools":[{"type":"function","function":{"name":"mock_bank-get_balance","description":"Get the current balance and owner for a bank account.","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"}},"required":["account_id"],"title":"get_balanceArguments","type":"object","additionalProperties":false},"strict":false}},{"type":"function","function":{"name":"mock_bank-list_transactions","description":"List recent transactions for a bank account, most recent first.","parameters":{"properties":{"account_id":{"title":"Account Id","type":"string"},"limit":{"default":5,"title":"Limit","type":"integer"}},"required":["account_id"],"title":"list_transactionsArguments","type":"object","additionalProperties":false},"strict":false}},{"type":"function","function":{"name":"mock_bank-transfer_funds","description":"Transfer funds between two mock bank accounts.","parameters":{"properties":{"from_account":{"title":"From Account","type":"string"},"to_account":{"title":"To Account","type":"string"},"amount":{"title":"Amount","type":"number"}},"required":["from_account","to_account","amount"],"title":"transfer_fundsArguments","type":"object","additionalProperties":false},"strict":false}}],"mcp_tool_calls":[{"function":{"arguments":"{\"account_id\":\"ACC1001\"}","name":"mock_bank-get_balance"},"id":"call_59tE386FwTOGOROXJNJPQZlB","type":"function"}],"mcp_call_results":[{"tool_call_id":"call_59tE386FwTOGOROXJNJPQZlB","result":"{\n  \"account_id\": \"ACC1001\",\n  \"owner\": \"Alice Johnson\",\n  \"balance\": 3250.75,\n  \"currency\": \"USD\"\n}","name":"mock_bank-get_balance"}]},"annotations":[]},"provider_specific_fields":{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"protected_material_code":{"detected":false,"filtered":false},"protected_material_text":{"detected":false,"filtered":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}}],"usage":{"completion_tokens":50,"prompt_tokens":332,"total_tokens":382,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"latency_checkpoint":{"engine_tbt_ms":5,"engine_ttft_ms":39,"engine_ttlt_ms":272,"pre_inference_ms":154,"service_tbt_ms":5,"service_ttft_ms":434,"service_ttlt_ms":646,"total_duration_ms":507,"user_visible_ttft_ms":281}},"service_tier":"default","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{}}]}
    

    При стандартном включении ограничений оба примера должны показывать адреса электронной почты в виде REDACTED. Если ограничения отключены, включите их в интерфейсе администратора.

    Практические пояснения

    1. Должен ли каждый запрос проходить через tools?

    Да, каждый раз, когда требуется использование MCP. Шлюз не добавляет инструменты автоматически к запросам, в которых они отсутствуют. Если пропустить tools, будет использоваться обычное завершение от LLM, даже если подключен mock_bank.

    2. tool_choice: "required" против "auto"

    Используйте "required" для простого тестирования вызова инструмента. Лучше выбирать "auto" для диалоговых или многократных операций, поскольку "required" по своей структуре не может самостоятельно сгенерировать окончательный ответ на естественном языке.

    3. Более безопасный и понятный выбор инструментов

    В противном случае клиентам необходимо знать точные названия инструментов и схемы, и ничто не мешает рискованному инструменту вроде transfer_funds выполняться так же легко, как get_balance. LiteLLM уже предлагает способы решения этой проблемы:

    a) Опубликовать каталог — не заставляйте интеграторов проводить обратную разработку вызовов модельных инструментов:

    curl -X POST 'http://localhost:4000/mcp/mock_bank' \
      -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
      -d '{"method":"tools/list"}'
    

    Предоставьте этот каталог (или /v1/mcp/tools) в документации для разработчиков или в интерфейсе настройки, чтобы названия, описания и схемы аргументов были видны до того, как кто-либо начнёт писать клиентский код.

    b) Вместо параметра allow_all_keys предпочитайте разрешения MCP по ключу/команде. Ограничьте доступ к mock_bank с помощью Guardrails/MCP → Permission Management, чтобы каждый ключ видел только те инструменты, которые ему положены.

    в) Разделение по уровню риска. Отметьте transfer_funds как инструмент для записи/с высоким риском и установите require_approval: „always“, чтобы человек подтверждал операцию перед переводом средств. Для инструментов для чтения оставьте значение „never“, если это соответствует вашей модели угроз.

    г) Обеспечение безопасности с помощью механизма Tool Permission Guardrail на стороне сервера, а не с помощью параметра tool_choice на стороне клиента. Пользователи контролируют tool_choice; это не является мерой безопасности. Механизм защиты на шлюзе не может быть проигнорирован приложением клиента:

    guardrails:
      - guardrail_name: "mcp-tool-permissions"
        litellm_params:
          guardrail: tool_permission
          mode: "pre_mcp_call"
          rules:
            - rule_id: "block-transfers"
              tool_name: "^mock_bank-transfer_fundsquot;
              decision: "deny"
          default_action: "allow"
    

    д) Ограничение аргументов, а не только списков разрешенных инструментов. Параметр allowed_params может ограничивать диапазоны amount или определять, какие значения account_id может использовать тот или иной инструмент — это более точно, чем простое полное или полное отказное решение.

    Относитесь к каталогу инструментов как к API продукта: публикуйте его, ограничивайте доступ по клиентам и использовайте правила с серверной стороны для контроля уровня риска при чтении и записи данных, вместо того чтобы доверять каждому клиенту.

    Что дальше

    После внедрения базовых механизмов защиты и MCP через LiteLLM логичными следующими шагами являются ограничения по частоте запросов, максимальное количество одновременных запросов, специфические проверки входных данных для MCP и более строгие правила выбора инструментов. Пока это не будет реализовано, храните демо-версии локально, ограничивайте доступ к рискованным инструментам согласованием, и тщательно определяйте, что может вызываться каждым API-ключом.

    При отладке нестабильных действий агента записывайте идентификатор запроса к шлюзу, строку имени зарегистрированной модели, названия серверов MCP, к которым был обращен запрос, а также информацию о том, было ли значение параметра tool_choice равно auto или required. Эти четыре параметра обычно позволяют быстрее выявить причины таких проблем, как «неверный идентификатор модели», «отсутствующие инструменты», «недоступность серверов обнаружения» и «частичная обработка параллельных запросов», чем повторное ручное выполнение операций. Используйте временный банковский счет: очищайте его и запускайте заново между тестами переводов, чтобы остатки средств не могли выдаваться за успешные новые транзакции.