Strona główna / Artykuły / Przekład narzędzi MCP za pomocą LiteLLM: Przewodnik po symulowanym bramku bankowym

Przekład narzędzi MCP za pomocą LiteLLM: Przewodnik po symulowanym bramku bankowym

Zarejestruj mock banku działającego przez protokół streamable-HTTP, porównaj pętle narzędzi stworzone ręcznie z jednym żądaniem do bramy, i udowodnij, że zasady bezpieczeństwa pre_call nigdy nie dotrą do MCP.

3709 słów

Zdjęcie: Quilia na Unsplash

Zakłada, że brama LiteLLM jest już uruchomiona (interfejs administracyjny oraz baza danych) z zarejestrowanym modeliem gpt-5.4-mini w Azure AI Foundry. Część opisowa dotyczy tej podstawowej konfiguracji.

Co oznacza „agentic” w LiteLLM

Brama MCP w LiteLLM umożliwia zarejestrowanemu modelowi dostęp do zewnętrznych narzędzi przez proxy. Brama odkrywa dostępne narzędzia, konwertuje schematy i zarządza procesem wykonywania, dzięki czemu kod aplikacji nie musi obsługiwać szczegółów protokołu MCP dla każdego dostawcy modeli. Twój klient komunikuje się z jednym punktem końcowym w stylu OpenAI; brama znajduje się pomiędzy tym klientem, modelem a wszystkimi zarejestrowanymi serwerami MCP.

Latwo pomylić identyfikatory modeli. W zależności od sposobu dodania wpisu do Azure, LiteLLM może wyświetlać azure_ai/gpt-5.4-mini zamiast prostego gpt-5.4-mini. Najpierw sprawdź aktualny rejestr:

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"}

Używaj dokładnie tego ciągu znaków przy każdej prośbie. Niezgodność objawia się jako Invalid model name passed in model=..., a nie jako ogólny błąd sieciowy.

Ten sam endpoint, zwykły LLM lub z narzędziami

Nie istnieje oddzielna ścieżka URL dla „trybu MCP”. /chat/completions i /v1/responses zachowują się tak samo zarówno w przypadku zwykłej rozmowy, jak i rozmowy wzbogaconej narzędziami. Jedyną różnicą jest to, czy ciało JSON zawiera tablicę tools (często z type: "mcp" dla narzędzi zarządzanych przez bramkę). Jeśli pominiesz tools, otrzymasz zwykłe uzupełnienie, nawet jeśli serwery MCP są podłączone do bramki. Jeśli dodasz tools, bramka może odkryć, wywołać te narzędzia i połączyć ich wyniki z kolejną turą rozmowy.

Zwykłe wezwanie bez narzędzi wygląda w ten sposób:

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"}}}]}

Częstym błędem przy kopiowaniu i wklejaniu jest dodatkowy cudzysłów przed nazwą modelu (""azure_ai/gpt-5.4-mini"). Powoduje to błąd Invalid JSON payload: unexpected character — problem z składnią treści, a nie uszkodzoną ścieżką.

The MCP Gateway, krok po kroku (przetestowane od początku do końca)

Symulowany serwer bankowy

Zamiast ogólnego przykładu procesu MCP „hello-world”, ten przewodnik wykorzystuje mały symulowany bank z trzema narzędziami — get_balance, list_transactions oraz transfer_funds — wspierany przez dwa konta w pamięci. Rzeczywiste salda i przelewy pozwalają szybko stwierdzić, czy pełna ścieżka zadziałała poprawnie. Każdy poniższy krok został przetestowany i sprawdzony na podstawie tych danych.

Krok 1 — uruchomienie serwera (zobacz także osobny plik mock_bank_server.py):

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

server.py korzysta z danych symulowanych, a nie z rzeczywistego rejestru. Zamierzony układ to: klient → LiteLLM → mechanizmy kontrolne → router → albo model Azure, albo symulowany proces MCP banku wraz z jego trzema narzędziami.

Kilka szczegółów dotyczących zachowania ma znaczenie po opuszczeniu diagramu:

  • Interesująca ścieżka to pętla, a nie pojedyncza strzałka. Model prosi o narzędzie; brama kieruje żądanie do MCP; wynik wraca przez tę samą bramę; model kontynuuje lub pyta ponownie. Procedury wykorzystujące pojedyncze narzędzie automatycznie zamykają tę pętlę; procedury równoległe wykorzystujące kilka narzędzi mogą być realizowane automatycznie tylko częściowo w testowanej tutaj wersji.
  • Zabezpieczenia są montowane w dwa miejscach. Funkcja pre_call sprawdza surowy treść rozmowy przed przekierowaniem. Funkcja pre_mcp_call analizuje argumenty narzędzi bliżej strony MCP. W diagramach często pokazuje się jedną ramkę; w rzeczywistości intercepcje odbywają się na różnych etapach.
  • Ramka z pociętymi liniami oznaczająca „narzędzia” to lista zawarta w pliku server.py, a nie trzy oddzielne mikrosługi.
  • Sztywny serwer MCP banku

    Mniej więcej dziewięćdziesiąt linijek kodu definiuje wersję FastMCP "mock-bank", która obejmuje:

    • get_balance(account_id) — informacje o właścicielu oraz saldzie
    • list_transactions(account_id, limit) — ostatnie transakcje
    • transfer_funds(from_account, to_account, amount) — przenoszenie środków z podstawową weryfikacją (nieznany rachunek, niewystarczające środki)
    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")
    

    Skrypty porównawcze wyjaśniają, co zastępuje bramka:

    • test-direct-mcp.py komunikuje się wyłącznie z serwerem MCP — sprawdza, czy narzędzia działają, i nie angażuje żadnego modelu.
    • test-direct-model.py bezpośrednio wywołuje Azure i MCP oraz ręcznie koordynuje konwersję schematu, wykonywanie narzędzi oraz drugą rundę komunikacji — około siedemdziesięciu pięciu linijek kodu pozwala gatewayowi zmienić strukturę na tablicę tools przy jednej żądaniu HTTP.
    • Żaden z tych skryptów nie korzysta z portu 4000; właśnie ta izolacja stanowi cel porównania przed i po modyfikacjach.

    Zapnij serwer i pozostaw go w trybie działania:

    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
    

    Krok 2 — dwie korekty dotyczące dostępności, zanim LiteLLM (w Dockerze) będzie mógł komunikować się z procesem hosta

    W kontenerze Compose/Admin UI localhost odnosi się do samego kontenera, a nie do twojego laptopa. Skieruj adres MCP na DNS host-forward Dockera:

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

    Odrębnie ochrona przed przekierowaniem DNS w SDK MCP może zwrócić błąd 421 Misdirected Request, chyba że przekazany adres hosta znajduje się na liście dozwolonych serwerem:

    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"],
        ),
    )
    

    Krok 3 — zarejestruj serwer w LiteLLM

    W interfejsie użytkownika: MCP Servers → Add MCP Server

    • Nazwa: mock_bank
    • Transport: Streamable HTTP (musi odpowiadać mcp.run(transport="streamable-http"))
    • URL: http://host.docker.internal:3001/mcp
    • Autoryzacja: nie wymagana w tym lokalnym przykładzie

    Lub poprzez plik konfiguracyjny:

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

    Potwierdź, że Connection Status: Connected, a także że w sekcji Tool Configuration widnieją wszystkie trzy włączone narzędzia.

    Krok 4 — sprawdź, jaki ciąg znaków modelu faktycznie zarejestrował bramkarz

    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"}
    

    Mimo przedrostka azure_ai/, ruch nadal trafiał do pipeline czatu Azure OpenAI podczas testów (w odpowiedziach pojawiały się pola filtrujące treść). Traktuj ten identyfikator jako osobliwość nazewnictwa, którą należy skopiować dosłownie, a nie jako błąd dostawcy.

    Krok 5 — jedno narzędzie w kolejności działania (czysta ścieżka)

    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
    

    Podczas udanego uruchomienia model wywołał funkcje get_balance i list_transactions, a następnie udzielił odpowiedzi przy użyciu danych symulacyjnych — Alice Johnson na ACC1001, salda zgodne z danymi w pamięci tymczasowej oraz trzy ostatnie transakcje z pliku mock_bank_server.py. To potwierdza sekwencję: brama → odkrywanie → wykonywanie → ostateczna odpowiedź.

    Krok 6 — wywołanie zmieniające stan, gdy jest to jedyne narzędzie w danej kolejności działania

    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":{}}]}
    

    Wynik: finish_reason: „stop”, transfer_funds zostało wykonyane automatycznie, ostateczny podsumowanie z aktualnymi saldami obu kont. Przekazywanie pieniędzy to dokładnie taka czynność, która wymaga zabezpieczeń, zanim cokolwiek opuści środowisko demonstracyjne.

    Lekcja — równoległe wywołania narzędzi w jednej turze

    Złożone polecenia, takie jak „przelej 200 dolarów z ACC1001 na ACC1002, a następnie potwierdź oba salda”, spowodowały, że model wykonał jednocześnie trzy wezwania narzędzi. W testowanej wersji LiteLLM automatyczne wykonanie MCP skutecznie zakończyło się tylko jednym wezwaniem (transfer_funds widniejącym w mcp_tool_calls / mcp_call_results), podczas gdy pozostałe wpisy get_balance pozostały nierozwiązane w tablicy tool_calls o strukturze OpenAI — bez żadnego końcowego tekstu, ponieważ model wciąż czekał. To zachowanie występowało zarówno w /v1/responses, jak i w /chat/completions, przy ustawieniu tool_choice na "required" lub "auto". Zatem nie jest to osobliwość związana z formą żądania; to sposób, w jaki ta wersja radziła sobie z wieloma równoległymi wezwaniami narzędzi MCP w jednej turze.

    Praktyczne wskazówki dotyczące tej konfiguracji: proszę o użycie jednego narzędzia na raz (jak w kroku 6). Jeśli naprawdę potrzebujesz równoległych wywołań, sam zrealizuj pętlę — uruchom pozostałe tool_calls i przeslij wyniki z role: „tool”, według tego samego wzoru pokazanego w poniższych skryptach Direct vs Gateway. Wybór modelu nadal ma znaczenie dla niezawodności systemu; „doskonały” wybór z wczoraj może stać się przestarzały po następnym cyklu aktualizacji, dlatego utrzymuj możliwość zmiany ścieżki orkiestracji.

    Krok 7 — sprawdzenie procesu odkrywania bez angażowania modelu

    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"}}]}}
    

    To wywołanie odróżnia przypadek „proces odkrywania nie działa” od sytuacji, gdy „model odmówił wywołania narzędzia” lub „wykonano tylko część operacji”.

    Direct vs Gateway: co daje proxy

    Test 1 — surowy MCP, bez modelu, bez gatewaya. Udowodnij działanie samego serwera bankowego:

    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())
    

    Oczekiwany wyświetlacz i wynik salda:

    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
        }
      ]
    }
    

    Test 2 — model bezpośredni w połączeniu z ręcznie zarządzaną orkiestracją. Pobieranie narzędzi MCP, konwersja schematów na format narzędzi OpenAI, wywoływanie usług Azure, uruchamianie narzędzi oraz przekazywanie wyników — aplikacja musi zawierać około siedemdziesięciu pięciu linijek kodu, bez żadnych wspólnych zasad bezpieczeństwa ani śledzenia wydatków:

    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())
    

    Przykład udanej odpowiedzi opisowej:

    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**
    

    Test 3 — ta sama intencja za pomocą LiteLLM.Jedna prośba HTTP; odkrywanie, konwersja, wykonywanie oraz kolejna odpowiedź odbywają się bezpośrednio w bramce:

    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}
    
    Sam ją utrzymujesz
    Kwestia Bezpośredni (Test 2) Brama (Test 3)
    Kod aplikacji ~75 linijek na aplikację Jedna prośba HTTP
    Konwersja schematów Ręczna, dla każdego serwera MCP Automaticzna
    Pętla narzędzi
    Wewnętrzne Zmiana dostawcy Ponowne napisanie wywołań SDK Zmiana ciągu znaków model Autoryzacja do wielu serwerów MCP Dostosowane indywidualnie dla każdego serwera Centralna konfiguracja mcp_servers Zasady ograniczające / wydatki Stwórz je samodzielnie Dostępne po konfiguracji

    Zasady takie jak redakcja adresów e-mail, blokowanie numerów kart i słów kluczowych działają w fazie pre_call — **przed** odkryciem lub wykonaniem MCP. Zablokowana prośba nie powinna kiedykolwiek dotknąć symulowanego banku. To właśnie tę cechę należy udowodnić, a nie tylko to, że tekst wygląda na zaszyfrowany w odpowiedzi.

    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":{}}]}
    

    Powtórz bez podawania nazw zasad w tekście użytkownika:

    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":{}}]}
    

    Gdy zasady są włączone domyślnie, oba powinny pokazywać adresy e-mail jako ZREDAGOWANE. Jeśli są wyłączone, włącz je w interfejsie administracyjnym.

    Praktyczne wyjaśnienia

    1. Czy każda prośba musi przechodzić przez tools?

    Tak, zawsze wtedy, gdy należy użyć MCP. Bramka nie dodaje potajemnie narzędzi do wywołań, które je pomijają. Jeśli pominiesz tools, otrzymasz zwykłe uzupełnienie od LLM, nawet jeśli jest podłączony mock_bank.

    2. tool_choice: „required” vs „auto”

    Użyj „required” do prostego testu sprawdzającego wywołanie narzędzia. Wolij „auto” w przypadku rozmów lub procesów wieloetapowych, ponieważ „required” strukturalnie nie może samodzielnie wygenerować ostatecznej odpowiedzi w języku naturalnym.

    3. Bezpieczniejszy, jaśniejszy wybór narzędzi

    Klienci muszą znać dokładne nazwy narzędzi i ich schematy, a nic nie przeszkadza ryzykownemu narzędziu takiemu jak transfer_funds w działaniu równie łatwo jak get_balance. LiteLLM już oferuje rozwiązania:

    a) Opublikuj katalog — nie zmuszaj integratorów do analizy kodu w celu odtworzenia wywołań narzędzi modelowych:

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

    Ujawnij ten katalog (lub /v1/mcp/tools) w dokumentacji dla programistów lub w interfejsie wprowadzania użytkowników, aby nazwy, opisy oraz schematy argumentów były widoczne przed napisaniem przez kogokolwiek kodu klienta.

    b) Wolij uprawnienia MCP typu „na klucz” lub „na zespół” zamiast allow_all_keys. Zastosuj ograniczenia dla mock_bank za pomocą Guardrails/MCP → Permission Management, aby każdy klucz widział tylko te narzędzia, które powinien.

    c) Podział według poziomu ryzyka. Oznacz transfer_funds jako narzędzie do zapisu o wysokim ryzyku i ustaw require_approval: „always”, aby osoba ludzka potwierdziła transakcję przed jej przeprowadzeniem. Narzędzia do odczytu pozostaw na ustawieniu „never”, jeśli odpowiada to twojemu modelowi zagrożeń.

    d) Egzekwuj zasady za pomocą narzędzia Tool Permission Guardrail po stronie serwera, a nie za pomocą opcji tool_choice po stronie klienta. Osoby korzystające z aplikacji kontrolują tool_choice; nie stanowi to bariery bezpieczeństwa. Narzędzie Gateway Guardrail nie może zostać pominięte przez aplikację klienta:

    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) Ogranicz argumenty, a nie tylko listy dozwolonych narzędzi. allowed_params może określać zakresy wartości amount lub ograniczać, jakie wartości account_id może dotyczyć dana funkcja — co jest bardziej precyzyjne niż proste odrzucenie wszystkiego lub niczego.

    Traktuj katalog narzędzi jak API produktu: publikuj go, dostosowuj jego zakres do poszczególnych klientów oraz określ poziom ryzyka przy operacjach odczytu i zapisu za pomocą polityki serwerowej, zamiast ufać każdemu klientowi.

    Kolejne kroki

    Gdy już wprowadzimy podstawowe zasady bezpieczeństwa oraz MCP poprzez LiteLLM, naturalnymi dalszymi krokami będą limity szybkości, maksymalna liczba jednoczesnych żądań, specyficzne dla MCP mechanizmy ochrony danych wejściowych oraz bardziej restrykcyjna polityka wyboru narzędzi. Dopóki to nie zostanie zrealizowane, przechowuj demonstracje lokalnie, trzymaj ryzykowne narzędzia poza zasięgiem bez zgody oraz starannie planuj, jakie funkcje może wywołać każdy klucz API.

    Gdy debugujesz niestabilne działanie agentów, zapisz identyfikator żądania bramy, nazwę zarejestrowanego modelu, nazwy serwerów MCP, do których doszło połączenie, oraz to, czy tool_choice miał wartość auto czy required. Ten zestaw informacji zazwyczaj szybciej pomaga odróżnić „niewłaściwy identyfikator modelu”, „pominięte narzędzia”, „awarię mechanizmu odkrywania” oraz „częściową eksploatację równoległych wywołań” niż ponowne ręczne uruchamianie zapytań. Używaj tymczasowego konta bankowego: usuwaj dane i restartuj je między testami transferów, aby pozostałe salda nie wyglądały jak udane nowe transakcje.