首页 / 文章 / 通过LiteLLM调用MCP工具:模拟银行网关操作指南

通过LiteLLM调用MCP工具:模拟银行网关操作指南

注册一个可流式 HTTP 模拟银行,将手动编写的工具循环与单次网关请求进行对比,从而证明预调用防护机制永远不会触及 MCP。

3709 词

图片来自 Unsplash 上的 Quilia

假设 LiteLLM 网关已启动(包含管理界面和数据库),并且 gpt-5.4-mini 已在 Azure AI Foundry 中注册。概述部分介绍了这一基础设置。

LiteLLM 中“代理型”功能的含义

LiteLLM 的 MCP 网关允许已注册的模型通过代理访问外部工具。该网关负责发现工具、转换数据结构,并执行相应流程,这样应用程序代码就不必为每个模型供应商处理 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模式或使用工具的模式

并不存在独立的“MCP模式”URL。/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网关:分步详解(已进行端到端测试)

模拟银行服务器

本示例没有使用通用的“hello-world”MCP流程,而是构建了一个小型模拟银行系统,包含三个工具:get_balance、list_transactions和transfer_funds,这些工具由两个内存中的账户支持。通过真实的余额和转账操作可以明确判断完整路径是否有效。以下每一步都已实际运行,并根据这些数据进行了验证。

第一步——启动服务器(也可查看独立的mock_bank_server.py文件):

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

server.py 使用的是模拟数据,而非真实的账本。其预期架构为:客户端 → LiteLLM → 规则约束层 → 路由器 → Azure模型或模拟银行MCP流程及其三个工具。

离开该架构图后,有一些行为细节需要注意:

  • 实际的流程是一个循环,而非单箭头结构。模型会请求某个工具;网关会将请求路由至MCP;结果再通过网关返回;模型随后会继续执行或再次发起请求。使用单个工具时,该循环会自动完成;而在当前测试的版本中,并行使用多个工具可能只能部分自动执行。
  • 防护机制在两个位置生效。pre_call会在路由之前检查原始聊天数据。pre_mcp_call则在对MCP更接近的环节检查工具参数。图表中常只显示一个框,但实际上这些拦截发生在不同阶段。
  • 虚线标注的“工具”框只是server.py中的列表,并非三个独立的微服务。
  • 模拟银行MCP服务器

    大约九十行代码构成了FastMCP的“mock-bank”实现,包括:

    • get_balance(account_id) ——返回账户所有者及余额
    • list_transactions(account_id, limit) ——列出最近的交易记录
    • transfer_funds(from_account, to_account, amount) ——执行资金转账并进行基本验证(如账户不存在、余额不足等)
    from mcp.server.fastmcp import FastMCP
    from mcp.server.transport_security import TransportSecuritySettings
    
    mcp = FastMCP(
        "mock-bank",
        host="127.0.0.1",
        port=3001,
        transport_security=TransportSecuritySettings(
            allowed_hosts=["localhost:3001", "127.0.0.1:3001", "host.docker.internal:3001"],
        ),
    )
    
    # --- In-memory mock data (resets every restart) ---
    
    ACCOUNTS = {
        "ACC1001": {"owner": "Alice Johnson", "balance": 4250.75, "currency": "USD"},
        "ACC1002": {"owner": "Bob Smith", "balance": 980.10, "currency": "USD"},
    }
    
    TRANSACTIONS = {
        "ACC1001": [
            {"date": "2026-07-20", "description": "Grocery Store", "amount": -84.32},
            {"date": "2026-07-18", "description": "Payroll Deposit", "amount": 2500.00},
            {"date": "2026-07-15", "description": "Electric Bill", "amount": -120.44},
        ],
        "ACC1002": [
            {"date": "2026-07-21", "description": "Coffee Shop", "amount": -6.75},
            {"date": "2026-07-19", "description": "Freelance Payment", "amount": 450.00},
        ],
    }
    
    
    @mcp.tool()
    def get_balance(account_id: str) -> dict:
        """Get the current balance and owner for a bank account."""
        account = ACCOUNTS.get(account_id)
        if not account:
            return {"error": f"Account '{account_id}' not found"}
        return {
            "account_id": account_id,
            "owner": account["owner"],
            "balance": account["balance"],
            "currency": account["currency"],
        }
    
    
    @mcp.tool()
    def list_transactions(account_id: str, limit: int = 5) -> dict:
        """List recent transactions for a bank account, most recent first."""
        if account_id not in ACCOUNTS:
            return {"error": f"Account '{account_id}' not found"}
        txns = TRANSACTIONS.get(account_id, [])[:limit]
        return {"account_id": account_id, "transactions": txns}
    
    
    @mcp.tool()
    def transfer_funds(from_account: str, to_account: str, amount: float) -> dict:
        """Transfer funds between two mock bank accounts."""
        if from_account not in ACCOUNTS:
            return {"error": f"Source account '{from_account}' not found"}
        if to_account not in ACCOUNTS:
            return {"error": f"Destination account '{to_account}' not found"}
        if amount <= 0:
            return {"error": "Transfer amount must be positive"}
        if ACCOUNTS[from_account]["balance"] < amount:
            return {"error": f"Insufficient funds in '{from_account}'"}
    
        ACCOUNTS[from_account]["balance"] -= amount
        ACCOUNTS[to_account]["balance"] += amount
    
        return {
            "status": "success",
            "from_account": from_account,
            "to_account": to_account,
            "amount": amount,
            "new_balance_from": ACCOUNTS[from_account]["balance"],
            "new_balance_to": ACCOUNTS[to_account]["balance"],
        }
    
    
    if __name__ == "__main__":
        mcp.run(transport="streamable-http")
    

    对比脚本有助于明确网关替代了哪些功能:

    • test-direct-mcp.py仅与MCP服务器通信——用于验证工具功能,不涉及任何模型。
    • test-direct-model.py直接调用Azure和MCP,并手动协调模式转换、工具执行以及二次交互——大约75行的代码被整合,使得网关通过一次HTTP调用就将结果放入tools数组中。
    • 这两个脚本都不会访问端口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指的是该容器本身,而非你的笔记本电脑。应将MCP URL指向Docker的host-forward DNS:

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

    另外,MCP SDK的DNS重定向保护功能会返回421 Misdirected Request错误,除非服务器上将转发的主机地址加入允许列表:

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

    步骤3 — 向LiteLLM注册服务器

    在用户界面中操作:MCP Servers → Add MCP Server

    • 名称:mock_bank
    • 传输协议:Streamable HTTP(必须与mcp.run(transport="streamable-http")一致)
    • URL地址:http://host.docker.internal:3001/mcp
    • 认证方式:此本地演示无需认证

    或者通过配置文件设置:

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

    确认连接状态为“已连接”,并且工具配置中列出了所有三个已启用的工具。

    步骤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上的Alice Johnson,账户余额与内存中的预设值一致,同时还包含了来自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自动执行仅能可靠地完成一次调用(mcp_tool_calls / mcp_call_results中显示为transfer_funds),而剩余的get_balance调用则滞留在类似OpenAI格式的tool_calls数组中未被处理——由于模型仍在等待,因此没有生成最终文本。这种现象既出现在/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,无模型,无Gateway。仅用银行服务器进行验证:

    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,执行这些工具,并将结果反馈回来——你的应用大约需要75行代码,且没有共享的规范或支出跟踪功能:

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

    默认启用限制措施时,两者都应显示已遮蔽的电子邮件地址。如果限制措施处于关闭状态,则需在管理界面中将其开启。

    实用说明

    1. 是否每个请求都必须包含 tools?

    是的,只要该轮次需要使用MCP,就必须包含。如果请求中未指定工具,网关不会自动为其添加工具。即使已连接 mock_bank,若省略 tools,也会仅得到普通的LLM回复。

    2. tool_choice: "required" 与 "auto"

    若只需进行简单的工具调用测试,可使用 "required";而对于对话式或多步骤流程,则建议使用 "auto",因为 "required" 本身无法独立生成最终的自然语言答案。

    3. 更安全、更清晰的工具选择方式

    否则,客户需要知晓确切的工具名称和架构,而像 transfer_funds 这样具有风险性的工具完全可能像 get_balance 一样轻易被调用。LiteLLM 已经提供了相应的解决方案:

    a) 发布工具目录 —— 避免让集成商反编译模型工具的调用方式:

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

    在开发者文档或入门界面中公开该目录(或 /v1/mcp/tools),以便在任何人编写客户端代码之前就能看到工具名称、描述及参数架构。

    b) 相较于 allow_all_keys,优先使用按密钥/团队划分的 MCP 权限。 通过 Guardrails/MCP → 权限管理功能限制 mock_bank 的访问范围,确保每个密钥只能看到其应有的工具。

    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:进行发布,按客户划分访问范围,并通过服务器端策略设定不同的读写权限等级,而非完全依赖每个客户端。

    下一步该做什么

    在建立了基本防护机制以及通过LiteLLM实现MCP之后,接下来的自然举措包括设置速率限制、最大并发请求数、针对MCP的输入校验规则,以及更严格的工具选择策略。在这些机制完善之前,应将演示功能限制在本地运行,将有风险的工具置于审批流程之后,并谨慎控制每个API密钥可调用的功能。

    在调试不稳定的智能体响应时,应记录网关请求ID、注册的模型字符串、所连接的MCP服务器名称,以及tool_choice参数是设置为auto还是required。这四项信息通常能比手动重新执行请求更快地区分“模型ID错误”、“遗漏工具”、“发现功能故障”以及“并行调用部分执行”等问题。请将模拟银行数据视为临时使用:在每次转账测试之间清空并重新启动,以避免剩余余额被误认为是成功的新的测试结果。