Skip to content

Commit 140ebe9

Browse files
committed
refactor: extract -32022 to constant and add missing fallback tests
1 parent e483ed3 commit 140ebe9

10 files changed

Lines changed: 178 additions & 4 deletions

File tree

packages/toolbox-core/src/toolbox_core/mcp_transport/v20241105/mcp.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ async def _send_request(
7171
# Check for JSON-RPC Error
7272
if json_resp and isinstance(json_resp, dict) and "error" in json_resp:
7373
err_val = json_resp["error"]
74-
if isinstance(err_val, dict) and err_val.get("code") == -32022:
74+
if (
75+
isinstance(err_val, dict)
76+
and err_val.get("code")
77+
== types.UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE
78+
):
7579
server_supported = err_val.get("data", {}).get("supported", [])
7680
client_supported = self._supported_protocols
7781
mutually_supported = [

packages/toolbox-core/src/toolbox_core/mcp_transport/v20241105/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ class ErrorData(_BaseMCPModel):
5555
data: Any | None = None
5656

5757

58+
UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE = -32022
59+
60+
5861
class JSONRPCError(_BaseMCPModel):
5962
jsonrpc: Literal["2.0"]
6063
id: str | int

packages/toolbox-core/src/toolbox_core/mcp_transport/v20250326/mcp.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ async def _send_request(
8585
# Check for JSON-RPC Error
8686
if json_resp and isinstance(json_resp, dict) and "error" in json_resp:
8787
err_val = json_resp["error"]
88-
if isinstance(err_val, dict) and err_val.get("code") == -32022:
88+
if (
89+
isinstance(err_val, dict)
90+
and err_val.get("code")
91+
== types.UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE
92+
):
8993
server_supported = err_val.get("data", {}).get("supported", [])
9094
client_supported = self._supported_protocols
9195
mutually_supported = [

packages/toolbox-core/src/toolbox_core/mcp_transport/v20250326/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ class ErrorData(_BaseMCPModel):
5555
data: Any | None = None
5656

5757

58+
UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE = -32022
59+
60+
5861
class JSONRPCError(_BaseMCPModel):
5962
jsonrpc: Literal["2.0"]
6063
id: str | int

packages/toolbox-core/src/toolbox_core/mcp_transport/v20250618/mcp.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ async def _send_request(
7676
# Check for JSON-RPC Error
7777
if json_resp and isinstance(json_resp, dict) and "error" in json_resp:
7878
err_val = json_resp["error"]
79-
if isinstance(err_val, dict) and err_val.get("code") == -32022:
79+
if (
80+
isinstance(err_val, dict)
81+
and err_val.get("code")
82+
== types.UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE
83+
):
8084
server_supported = err_val.get("data", {}).get("supported", [])
8185
client_supported = self._supported_protocols
8286
mutually_supported = [

packages/toolbox-core/src/toolbox_core/mcp_transport/v20250618/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ class ErrorData(_BaseMCPModel):
5555
data: Any | None = None
5656

5757

58+
UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE = -32022
59+
60+
5861
class JSONRPCError(_BaseMCPModel):
5962
jsonrpc: Literal["2.0"]
6063
id: str | int

packages/toolbox-core/src/toolbox_core/mcp_transport/v20251125/mcp.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ async def _send_request(
7676
# Check for JSON-RPC Error
7777
if json_resp and isinstance(json_resp, dict) and "error" in json_resp:
7878
err_val = json_resp["error"]
79-
if isinstance(err_val, dict) and err_val.get("code") == -32022:
79+
if (
80+
isinstance(err_val, dict)
81+
and err_val.get("code")
82+
== types.UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE
83+
):
8084
server_supported = err_val.get("data", {}).get("supported", [])
8185
client_supported = self._supported_protocols
8286
mutually_supported = [

packages/toolbox-core/src/toolbox_core/mcp_transport/v20251125/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ class ErrorData(_BaseMCPModel):
5555
data: Any | None = None
5656

5757

58+
UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE = -32022
59+
60+
5861
class JSONRPCError(_BaseMCPModel):
5962
jsonrpc: Literal["2.0"]
6063
id: str | int

packages/toolbox-core/tests/mcp_transport/test_v20250618.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,79 @@ def get_result_model(self):
176176
with pytest.raises(RuntimeError, match="MCP request failed"):
177177
await transport._send_request("url", TestRequest())
178178

179+
async def test_version_negotiation_raises_fallback_200_ok(self, transport):
180+
"""Tests that the client raises ProtocolNegotiationError when the server returns 200 OK with -32022."""
181+
from toolbox_core.exceptions import ProtocolNegotiationError
182+
183+
mock_response_reject = AsyncMock()
184+
mock_response_reject.ok = True
185+
mock_response_reject.status = 200
186+
mock_response_reject.content.at_eof = MagicMock(return_value=False)
187+
mock_response_reject.json.return_value = {
188+
"jsonrpc": "2.0",
189+
"id": "1",
190+
"error": {
191+
"code": -32022,
192+
"message": "Unsupported protocol version",
193+
"data": {"supported": ["DRAFT-2026-v1"]},
194+
},
195+
}
196+
197+
transport._session.post.return_value.__aenter__.return_value = (
198+
mock_response_reject
199+
)
200+
201+
class TestResult(types.BaseModel):
202+
pass
203+
204+
class TestRequest(types.MCPRequest[TestResult]):
205+
method: str = "method"
206+
params: dict = {}
207+
208+
def get_result_model(self):
209+
return TestResult
210+
211+
with pytest.raises(ProtocolNegotiationError) as exc_info:
212+
await transport._send_request("url", TestRequest())
213+
214+
assert exc_info.value.negotiated_version == "DRAFT-2026-v1"
215+
assert transport._session.post.call_count == 1
216+
217+
async def test_version_negotiation_empty_intersection(self, transport):
218+
"""Tests that the client errors immediately without retrying when there is no mutual version."""
219+
mock_response_reject = AsyncMock()
220+
mock_response_reject.ok = True
221+
mock_response_reject.status = 200
222+
mock_response_reject.content.at_eof = MagicMock(return_value=False)
223+
mock_response_reject.json.return_value = {
224+
"jsonrpc": "2.0",
225+
"id": "1",
226+
"error": {
227+
"code": -32022,
228+
"message": "Unsupported protocol version",
229+
"data": {"supported": ["UNSUPPORTED-VERSION"]},
230+
},
231+
}
232+
233+
transport._session.post.return_value.__aenter__.return_value = (
234+
mock_response_reject
235+
)
236+
237+
class TestResult(types.BaseModel):
238+
pass
239+
240+
class TestRequest(types.MCPRequest[TestResult]):
241+
method: str = "method"
242+
params: dict = {}
243+
244+
def get_result_model(self):
245+
return TestResult
246+
247+
with pytest.raises(
248+
RuntimeError, match="No mutually supported protocol version"
249+
):
250+
await transport._send_request("url", TestRequest())
251+
179252
async def test_send_notification(self, transport):
180253
mock_response = AsyncMock()
181254
mock_response.ok = True

packages/toolbox-core/tests/mcp_transport/test_v20251125.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,79 @@ def get_result_model(self):
176176
with pytest.raises(RuntimeError, match="MCP request failed"):
177177
await transport._send_request("url", TestRequest())
178178

179+
async def test_version_negotiation_raises_fallback_200_ok(self, transport):
180+
"""Tests that the client raises ProtocolNegotiationError when the server returns 200 OK with -32022."""
181+
from toolbox_core.exceptions import ProtocolNegotiationError
182+
183+
mock_response_reject = AsyncMock()
184+
mock_response_reject.ok = True
185+
mock_response_reject.status = 200
186+
mock_response_reject.content.at_eof = MagicMock(return_value=False)
187+
mock_response_reject.json.return_value = {
188+
"jsonrpc": "2.0",
189+
"id": "1",
190+
"error": {
191+
"code": -32022,
192+
"message": "Unsupported protocol version",
193+
"data": {"supported": ["DRAFT-2026-v1"]},
194+
},
195+
}
196+
197+
transport._session.post.return_value.__aenter__.return_value = (
198+
mock_response_reject
199+
)
200+
201+
class TestResult(types.BaseModel):
202+
pass
203+
204+
class TestRequest(types.MCPRequest[TestResult]):
205+
method: str = "method"
206+
params: dict = {}
207+
208+
def get_result_model(self):
209+
return TestResult
210+
211+
with pytest.raises(ProtocolNegotiationError) as exc_info:
212+
await transport._send_request("url", TestRequest())
213+
214+
assert exc_info.value.negotiated_version == "DRAFT-2026-v1"
215+
assert transport._session.post.call_count == 1
216+
217+
async def test_version_negotiation_empty_intersection(self, transport):
218+
"""Tests that the client errors immediately without retrying when there is no mutual version."""
219+
mock_response_reject = AsyncMock()
220+
mock_response_reject.ok = True
221+
mock_response_reject.status = 200
222+
mock_response_reject.content.at_eof = MagicMock(return_value=False)
223+
mock_response_reject.json.return_value = {
224+
"jsonrpc": "2.0",
225+
"id": "1",
226+
"error": {
227+
"code": -32022,
228+
"message": "Unsupported protocol version",
229+
"data": {"supported": ["UNSUPPORTED-VERSION"]},
230+
},
231+
}
232+
233+
transport._session.post.return_value.__aenter__.return_value = (
234+
mock_response_reject
235+
)
236+
237+
class TestResult(types.BaseModel):
238+
pass
239+
240+
class TestRequest(types.MCPRequest[TestResult]):
241+
method: str = "method"
242+
params: dict = {}
243+
244+
def get_result_model(self):
245+
return TestResult
246+
247+
with pytest.raises(
248+
RuntimeError, match="No mutually supported protocol version"
249+
):
250+
await transport._send_request("url", TestRequest())
251+
179252
async def test_send_notification(self, transport):
180253
mock_response = AsyncMock()
181254
mock_response.ok = True

0 commit comments

Comments
 (0)