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

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

Зареєструйте мок-банк через Streamable-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: крок за кроком (тестовано від початку до кінця)

Штучний банківський сервер

Замість звичайного процесу типу “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. На діаграмах часто показано одну коробку; насправді це різні етапи перехоплення даних.
  • Коробка з пунктирною лінією „tools“ — це список у файлі 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 у MCP SDK може повертати код 421 Misdirected Request, якщо вказаний Host не внесений до списку дозволених на сервері:

    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
    

    Переконайтеся, що статус з’єднання — 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) Віддавайте перевагу дозволам MCP на рівні ключа/команди замість allow_all_keys. Обмежте доступ до mock_bank через Guardrails/MCP → Permission Management, щоб кожен ключ бачив лише ті інструменти, які йому потрібні.

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

    d) Забезпечуйте безпеку за допомогою захисних механізмів на серверній стороні, а не за рахунок параметра 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"
    

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

    Ставіться до каталогу інструментів як до API продукту: публікуйте його, налаштовуйте доступ для кожного клієнта та встановлюйте рівні ризику для операцій читання та запису за допомогою політик на серверному рівні, замість того щоб довіряти кожному клієнту.

    Що далі

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

    Під час виправлення нестабільних дій агента записуйте ідентифікатор запиту до шлюзу, рядок імені зареєстрованої моделі, назви серверів MCP, які були підключені, а також те, чи було значення tool_choice рівним auto чи required. Ці чотири параметри зазвичай допомагають швидше виявити проблеми типу „неправильний ідентифікатор моделі“, „відсутні інструменти“, „проблеми з пошуком“ чи „часткова обробка паралельних запитів“, ніж повторне виконання операцій вручну. Використовуйте тимчасовий банк даних: очищуйте його та запускайте заново між тестами переказів, щоб залишки балансу не могли видаватися успішними новими демонстраціями.