Skip to content

Commit d83b9d5

Browse files
committed
feat: add automated protocol version fallback negotiation for invalid protocol version errors across all MCP transports
1 parent 91de13e commit d83b9d5

9 files changed

Lines changed: 412 additions & 51 deletions

File tree

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

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from ... import version
2121
from ...exceptions import ProtocolNegotiationError
22-
from ...protocol import ManifestSchema, TelemetryAttributes
22+
from ...protocol import ManifestSchema, Protocol, TelemetryAttributes
2323
from .. import telemetry
2424
from ..transport_base import _McpHttpTransportBase
2525
from . import types
@@ -53,20 +53,23 @@ async def _send_request(
5353
async with self._session.post(
5454
url, json=payload, headers=dict(headers or {})
5555
) as response:
56+
json_resp = None
5657
if not response.ok:
57-
error_text = await response.text()
58-
raise RuntimeError(
59-
f"API request failed with status {response.status} "
60-
f"({response.reason}). Server response: {error_text}"
61-
)
62-
63-
if response.status == 204 or response.content.at_eof():
64-
return None
65-
66-
json_resp = await response.json()
58+
try:
59+
json_resp = await response.json()
60+
except Exception:
61+
error_text = await response.text()
62+
raise RuntimeError(
63+
f"API request failed with status {response.status} "
64+
f"({response.reason}). Server response: {error_text}"
65+
)
66+
else:
67+
if response.status == 204 or response.content.at_eof():
68+
return None
69+
json_resp = await response.json()
6770

6871
# Check for JSON-RPC Error
69-
if "error" in json_resp:
72+
if json_resp and isinstance(json_resp, dict) and "error" in json_resp:
7073
err_val = json_resp["error"]
7174
if isinstance(err_val, dict) and err_val.get("code") == -32022:
7275
server_supported = err_val.get("data", {}).get("supported", [])
@@ -82,6 +85,30 @@ async def _send_request(
8285
f"Client supports: {client_supported}, "
8386
f"Server supports: {server_supported}"
8487
)
88+
elif (
89+
isinstance(err_val, str)
90+
and "invalid protocol version" in err_val.lower()
91+
):
92+
client_supported = (
93+
self._supported_protocols
94+
or Protocol.get_supported_mcp_versions()
95+
)
96+
try:
97+
current_idx = client_supported.index(self._protocol_version)
98+
if current_idx + 1 < len(client_supported):
99+
raise ProtocolNegotiationError(
100+
client_supported[current_idx + 1]
101+
)
102+
else:
103+
raise RuntimeError(
104+
"Server threw 'invalid protocol version' but no fallback versions "
105+
"remain in the user's supported protocols array."
106+
)
107+
except ValueError:
108+
raise RuntimeError(
109+
f"Invalid state: current protocol {self._protocol_version} is not in supported_protocols."
110+
)
111+
85112
try:
86113
err = types.JSONRPCError.model_validate(json_resp).error
87114
raise RuntimeError(
@@ -90,6 +117,12 @@ async def _send_request(
90117
except Exception:
91118
raise RuntimeError(f"MCP request failed: {json_resp.get('error')}")
92119

120+
if not response.ok:
121+
raise RuntimeError(
122+
f"API request failed with status {response.status} ({response.reason}). "
123+
f"Server response: {json_resp}"
124+
)
125+
93126
# Parse Result
94127
if isinstance(request, types.MCPRequest):
95128
try:

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

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from ... import version
2121
from ...exceptions import ProtocolNegotiationError
22-
from ...protocol import ManifestSchema, TelemetryAttributes
22+
from ...protocol import ManifestSchema, Protocol, TelemetryAttributes
2323
from .. import telemetry
2424
from ..transport_base import _McpHttpTransportBase
2525
from . import types
@@ -66,21 +66,24 @@ async def _send_request(
6666
if request.method == "initialize" and "Mcp-Session-Id" in response.headers:
6767
self._session_id = response.headers["Mcp-Session-Id"]
6868

69+
json_resp = None
6970
if not response.ok:
70-
error_text = await response.text()
71-
raise RuntimeError(
72-
"API request failed with status"
73-
f" {response.status} ({response.reason}). Server response:"
74-
f" {error_text}"
75-
)
76-
77-
if response.status == 204 or response.content.at_eof():
78-
return None
79-
80-
json_resp = await response.json()
71+
try:
72+
json_resp = await response.json()
73+
except Exception:
74+
error_text = await response.text()
75+
raise RuntimeError(
76+
"API request failed with status"
77+
f" {response.status} ({response.reason}). Server response:"
78+
f" {error_text}"
79+
)
80+
else:
81+
if response.status == 204 or response.content.at_eof():
82+
return None
83+
json_resp = await response.json()
8184

8285
# Check for JSON-RPC Error
83-
if "error" in json_resp:
86+
if json_resp and isinstance(json_resp, dict) and "error" in json_resp:
8487
err_val = json_resp["error"]
8588
if isinstance(err_val, dict) and err_val.get("code") == -32022:
8689
server_supported = err_val.get("data", {}).get("supported", [])
@@ -96,6 +99,30 @@ async def _send_request(
9699
f"Client supports: {client_supported}, "
97100
f"Server supports: {server_supported}"
98101
)
102+
elif (
103+
isinstance(err_val, str)
104+
and "invalid protocol version" in err_val.lower()
105+
):
106+
client_supported = (
107+
self._supported_protocols
108+
or Protocol.get_supported_mcp_versions()
109+
)
110+
try:
111+
current_idx = client_supported.index(self._protocol_version)
112+
if current_idx + 1 < len(client_supported):
113+
raise ProtocolNegotiationError(
114+
client_supported[current_idx + 1]
115+
)
116+
else:
117+
raise RuntimeError(
118+
"Server threw 'invalid protocol version' but no fallback versions "
119+
"remain in the user's supported protocols array."
120+
)
121+
except ValueError:
122+
raise RuntimeError(
123+
f"Invalid state: current protocol {self._protocol_version} is not in supported_protocols."
124+
)
125+
99126
try:
100127
err = types.JSONRPCError.model_validate(json_resp).error
101128
raise RuntimeError(
@@ -106,6 +133,12 @@ async def _send_request(
106133
raw_error = json_resp.get("error", {})
107134
raise RuntimeError(f"MCP request failed: {raw_error}")
108135

136+
if not response.ok:
137+
raise RuntimeError(
138+
f"API request failed with status {response.status} ({response.reason}). "
139+
f"Server response: {json_resp}"
140+
)
141+
109142
# Parse Result
110143
if isinstance(request, types.MCPRequest):
111144
try:

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

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,24 @@ async def _send_request(
5757
async with self._session.post(
5858
url, json=payload, headers=req_headers
5959
) as response:
60+
json_resp = None
6061
if not response.ok:
61-
error_text = await response.text()
62-
raise RuntimeError(
63-
"API request failed with status"
64-
f" {response.status} ({response.reason}). Server response:"
65-
f" {error_text}"
66-
)
67-
68-
if response.status == 204 or response.content.at_eof():
69-
return None
70-
71-
json_resp = await response.json()
62+
try:
63+
json_resp = await response.json()
64+
except Exception:
65+
error_text = await response.text()
66+
raise RuntimeError(
67+
"API request failed with status"
68+
f" {response.status} ({response.reason}). Server response:"
69+
f" {error_text}"
70+
)
71+
else:
72+
if response.status == 204 or response.content.at_eof():
73+
return None
74+
json_resp = await response.json()
7275

7376
# Check for JSON-RPC Error
74-
if "error" in json_resp:
77+
if json_resp and isinstance(json_resp, dict) and "error" in json_resp:
7578
err_val = json_resp["error"]
7679
if isinstance(err_val, dict) and err_val.get("code") == -32022:
7780
server_supported = err_val.get("data", {}).get("supported", [])
@@ -87,6 +90,30 @@ async def _send_request(
8790
f"Client supports: {client_supported}, "
8891
f"Server supports: {server_supported}"
8992
)
93+
elif (
94+
isinstance(err_val, str)
95+
and "invalid protocol version" in err_val.lower()
96+
):
97+
client_supported = (
98+
self._supported_protocols
99+
or Protocol.get_supported_mcp_versions()
100+
)
101+
try:
102+
current_idx = client_supported.index(self._protocol_version)
103+
if current_idx + 1 < len(client_supported):
104+
raise ProtocolNegotiationError(
105+
client_supported[current_idx + 1]
106+
)
107+
else:
108+
raise RuntimeError(
109+
"Server threw 'invalid protocol version' but no fallback versions "
110+
"remain in the user's supported protocols array."
111+
)
112+
except ValueError:
113+
raise RuntimeError(
114+
f"Invalid state: current protocol {self._protocol_version} is not in supported_protocols."
115+
)
116+
90117
try:
91118
err = types.JSONRPCError.model_validate(json_resp).error
92119
raise RuntimeError(
@@ -97,6 +124,12 @@ async def _send_request(
97124
raw_error = json_resp.get("error", {})
98125
raise RuntimeError(f"MCP request failed: {raw_error}")
99126

127+
if not response.ok:
128+
raise RuntimeError(
129+
f"API request failed with status {response.status} ({response.reason}). "
130+
f"Server response: {json_resp}"
131+
)
132+
100133
# Parse Result
101134
if isinstance(request, types.MCPRequest):
102135
try:

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

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,24 @@ async def _send_request(
5757
async with self._session.post(
5858
url, json=payload, headers=req_headers
5959
) as response:
60+
json_resp = None
6061
if not response.ok:
61-
error_text = await response.text()
62-
raise RuntimeError(
63-
"API request failed with status"
64-
f" {response.status} ({response.reason}). Server response:"
65-
f" {error_text}"
66-
)
67-
68-
if response.status == 204 or response.content.at_eof():
69-
return None
70-
71-
json_resp = await response.json()
62+
try:
63+
json_resp = await response.json()
64+
except Exception:
65+
error_text = await response.text()
66+
raise RuntimeError(
67+
"API request failed with status"
68+
f" {response.status} ({response.reason}). Server response:"
69+
f" {error_text}"
70+
)
71+
else:
72+
if response.status == 204 or response.content.at_eof():
73+
return None
74+
json_resp = await response.json()
7275

7376
# Check for JSON-RPC Error
74-
if "error" in json_resp:
77+
if json_resp and isinstance(json_resp, dict) and "error" in json_resp:
7578
err_val = json_resp["error"]
7679
if isinstance(err_val, dict) and err_val.get("code") == -32022:
7780
server_supported = err_val.get("data", {}).get("supported", [])
@@ -87,6 +90,30 @@ async def _send_request(
8790
f"Client supports: {client_supported}, "
8891
f"Server supports: {server_supported}"
8992
)
93+
elif (
94+
isinstance(err_val, str)
95+
and "invalid protocol version" in err_val.lower()
96+
):
97+
client_supported = (
98+
self._supported_protocols
99+
or Protocol.get_supported_mcp_versions()
100+
)
101+
try:
102+
current_idx = client_supported.index(self._protocol_version)
103+
if current_idx + 1 < len(client_supported):
104+
raise ProtocolNegotiationError(
105+
client_supported[current_idx + 1]
106+
)
107+
else:
108+
raise RuntimeError(
109+
"Server threw 'invalid protocol version' but no fallback versions "
110+
"remain in the user's supported protocols array."
111+
)
112+
except ValueError:
113+
raise RuntimeError(
114+
f"Invalid state: current protocol {self._protocol_version} is not in supported_protocols."
115+
)
116+
90117
try:
91118
err = types.JSONRPCError.model_validate(json_resp).error
92119
raise RuntimeError(
@@ -97,6 +124,12 @@ async def _send_request(
97124
raw_error = json_resp.get("error", {})
98125
raise RuntimeError(f"MCP request failed: {raw_error}")
99126

127+
if not response.ok:
128+
raise RuntimeError(
129+
f"API request failed with status {response.status} ({response.reason}). "
130+
f"Server response: {json_resp}"
131+
)
132+
100133
# Parse Result
101134
if isinstance(request, types.MCPRequest):
102135
try:

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,9 @@ async def _send_request(
137137
"remain in the user's supported protocols array."
138138
)
139139
except ValueError:
140-
# Current version not in list somehow, just fallback to highest stateful
141-
raise ProtocolNegotiationError(Protocol.MCP_v20251125)
140+
raise RuntimeError(
141+
f"Invalid state: current protocol {self._protocol_version} is not in supported_protocols."
142+
)
142143

143144
try:
144145
err = types.JSONRPCError.model_validate(json_resp).error

0 commit comments

Comments
 (0)