litellm/tests/integration/mcp/test_mcp_protocol_errors.py
Yuneng Jiang c00f1b4a5c
Some checks failed
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
test: add extension and browser integration contracts
Adds integration contracts for MCP lifecycle, protocol errors and OAuth
configuration, A2A wire versions, the OpenAI consumer path, persisted
toolsets, callback delivery, guardrail effects, configured prices, the
filtered spend ledger, and a CircleCI-owned browser flow for project
detachment, with the ASGI, browser-state, client and MCP helpers they use.
Consolidates the eleven commits previously stacked on
litellm_integration_providers onto its rebased tip
2026-09-16 13:15:53 -07:00

86 lines
3.6 KiB
Python

import json
import queue
import uuid
from typing import Final
import pytest
from integration._support.client import Gateway
from integration._support.mcp import McpPeer, call_tool, register_mcp, tool_names
from integration._support.wire import Reply, Request, wire_server
@pytest.mark.covers("other.mcp.errors.protocol_and_malformed_results_cannot_be_empty_success")
def test_jsonrpc_error_and_malformed_tool_result_remain_errors(gateway: Gateway) -> None:
def provider(request: Request) -> Reply:
if request.method != "POST":
return Reply(status=405)
body: Final = json.loads(request.body)
method: Final = body["method"]
if "id" not in body:
return Reply(status=202)
base: Final = {"jsonrpc": "2.0", "id": body["id"]}
if method == "initialize":
return Reply(
body=json.dumps(
{
**base,
"result": {
"protocolVersion": body["params"]["protocolVersion"],
"capabilities": {"tools": {}},
"serverInfo": {"name": "synthetic-protocol-peer", "version": "1"},
},
}
).encode()
)
if method == "tools/list":
return Reply(
body=json.dumps(
{
**base,
"result": {
"tools": [
{"name": name, "inputSchema": {"type": "object"}}
for name in ("add", "multiply", "fail")
]
},
}
).encode()
)
assert method == "tools/call"
name: Final = body["params"]["name"]
if name == "fail":
return Reply(
body=json.dumps({**base, "error": {"code": -32042, "message": "synthetic JSON-RPC error"}}).encode()
)
result: Final = (
{"content": "synthetic malformed content"}
if name == "multiply"
else {"content": [{"type": "text", "text": "8"}], "isError": False}
)
return Reply(body=json.dumps({**base, "result": result}).encode())
with wire_server(provider) as wire, gateway.scenario() as scenario:
identity: Final = register_mcp(
scenario, McpPeer(wire.url + "/mcp", queue.Queue()), "integration" + uuid.uuid4().hex
)
key: Final = scenario.key(object_permission={"mcp_servers": [identity]})
names: Final = tool_names(gateway, key, identity)
for name, expected in (("fail", "synthetic JSON-RPC error"), ("multiply", "validation")):
wire.drain()
response: Final = call_tool(gateway, key, identity, names[name], {})
assert response.status_code == 200 and response.json()["isError"] is True, response.text
assert expected.lower() in response.json()["content"][0]["text"].lower(), response.text
assert (
len(
tuple(
item
for item in wire.drain()
if item.method == "POST" and json.loads(item.body).get("method") == "tools/call"
)
)
== 1
)
control: Final = call_tool(gateway, key, identity, names["add"], {"a": 3, "b": 5})
assert control.status_code == 200 and control.json()["isError"] is False, control.text
assert control.json()["content"][0]["text"] == "8"