Coverage for mcpgateway / validation / jsonrpc.py: 100%
58 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-11 07:10 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-02-11 07:10 +0000
1# -*- coding: utf-8 -*-
2"""Location: ./mcpgateway/validation/jsonrpc.py
3Copyright 2025
4SPDX-License-Identifier: Apache-2.0
5Authors: Mihai Criveti
7JSON-RPC Validation.
8This module provides validation functions for JSON-RPC 2.0 requests and responses
9according to the specification at https://www.jsonrpc.org/specification.
11Includes:
12- Request validation
13- Response validation
14- Standard error codes
15- Error message formatting
17Examples:
18 >>> from mcpgateway.validation.jsonrpc import JSONRPCError, validate_request
19 >>> error = JSONRPCError(-32600, "Invalid Request")
20 >>> error.code
21 -32600
22 >>> error.message
23 'Invalid Request'
24 >>> validate_request({'jsonrpc': '2.0', 'method': 'test', 'id': 1})
25 >>> validate_request({'jsonrpc': '2.0', 'method': 'test'}) # notification
26 >>> try:
27 ... validate_request({'method': 'test'}) # missing jsonrpc
28 ... except JSONRPCError as e:
29 ... e.code
30 -32600
31"""
33# Standard
34from typing import Any, Dict, Optional, Union
37class JSONRPCError(Exception):
38 """JSON-RPC protocol error."""
40 def __init__(
41 self,
42 code: int,
43 message: str,
44 data: Optional[Any] = None,
45 request_id: Optional[Union[str, int]] = None,
46 ):
47 """Initialize JSON-RPC error.
49 Args:
50 code: Error code
51 message: Error message
52 data: Optional error data
53 request_id: Optional request ID
54 """
55 self.code = code
56 self.message = message
57 self.data = data
58 self.request_id = request_id
59 super().__init__(message)
61 def to_dict(self) -> Dict[str, Any]:
62 """Convert error to JSON-RPC error response dict.
64 Returns:
65 Error response dictionary
67 Examples:
68 Basic error without data:
69 >>> error = JSONRPCError(-32600, "Invalid Request", request_id=1)
70 >>> error.to_dict()
71 {'jsonrpc': '2.0', 'error': {'code': -32600, 'message': 'Invalid Request'}, 'request_id': 1}
73 Error with additional data:
74 >>> error = JSONRPCError(-32602, "Invalid params", data={"param": "value"}, request_id="abc")
75 >>> error.to_dict()
76 {'jsonrpc': '2.0', 'error': {'code': -32602, 'message': 'Invalid params', 'data': {'param': 'value'}}, 'request_id': 'abc'}
78 Error without request ID (for parse errors):
79 >>> error = JSONRPCError(-32700, "Parse error", data="Unexpected EOF")
80 >>> error.to_dict()
81 {'jsonrpc': '2.0', 'error': {'code': -32700, 'message': 'Parse error', 'data': 'Unexpected EOF'}, 'request_id': None}
83 Error with complex data:
84 >>> error = JSONRPCError(-32603, "Internal error", data={"details": ["error1", "error2"], "timestamp": 123456}, request_id=42)
85 >>> sorted(error.to_dict()['error']['data']['details'])
86 ['error1', 'error2']
87 """
88 error = {"code": self.code, "message": self.message}
89 if self.data is not None:
90 error["data"] = self.data
92 return {"jsonrpc": "2.0", "error": error, "request_id": self.request_id}
95# Standard JSON-RPC error codes
96PARSE_ERROR = -32700 #: Invalid JSON
97INVALID_REQUEST = -32600 #: Invalid Request object
98METHOD_NOT_FOUND = -32601 #: Method not found
99INVALID_PARAMS = -32602 #: Invalid method parameters
100INTERNAL_ERROR = -32603 #: Internal JSON-RPC error
101SERVER_ERROR_START = -32000 #: Start of server error codes
102SERVER_ERROR_END = -32099 #: End of server error codes
105def validate_request(request: Dict[str, Any]) -> None:
106 """Validate JSON-RPC request.
108 Args:
109 request: Request dictionary to validate
111 Raises:
112 JSONRPCError: If request is invalid
114 Examples:
115 Valid request:
116 >>> validate_request({"jsonrpc": "2.0", "method": "ping", "id": 1})
118 Valid notification (no id):
119 >>> validate_request({"jsonrpc": "2.0", "method": "notify"})
121 Valid request with params:
122 >>> validate_request({"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1})
123 >>> validate_request({"jsonrpc": "2.0", "method": "add", "params": {"a": 1, "b": 2}, "id": 1})
125 Invalid version:
126 >>> validate_request({"jsonrpc": "1.0", "method": "ping", "id": 1}) # doctest: +ELLIPSIS
127 Traceback (most recent call last):
128 ...
129 mcpgateway.validation.jsonrpc.JSONRPCError: Invalid JSON-RPC version
131 Missing method:
132 >>> validate_request({"jsonrpc": "2.0", "id": 1}) # doctest: +ELLIPSIS
133 Traceback (most recent call last):
134 ...
135 mcpgateway.validation.jsonrpc.JSONRPCError: Invalid or missing method
137 Empty method:
138 >>> validate_request({"jsonrpc": "2.0", "method": "", "id": 1}) # doctest: +ELLIPSIS
139 Traceback (most recent call last):
140 ...
141 mcpgateway.validation.jsonrpc.JSONRPCError: Invalid or missing method
143 Invalid params type:
144 >>> validate_request({"jsonrpc": "2.0", "method": "test", "params": "invalid", "id": 1}) # doctest: +ELLIPSIS
145 Traceback (most recent call last):
146 ...
147 mcpgateway.validation.jsonrpc.JSONRPCError: Invalid params type
149 Invalid ID type:
150 >>> validate_request({"jsonrpc": "2.0", "method": "test", "id": True}) # doctest: +ELLIPSIS
151 Traceback (most recent call last):
152 ...
153 mcpgateway.validation.jsonrpc.JSONRPCError: Invalid request ID type
154 """ # doctest: +ELLIPSIS
155 # Check jsonrpc version
156 if request.get("jsonrpc") != "2.0":
157 raise JSONRPCError(INVALID_REQUEST, "Invalid JSON-RPC version", request_id=request.get("id"))
159 # Check method
160 method = request.get("method")
161 if not isinstance(method, str) or not method:
162 raise JSONRPCError(INVALID_REQUEST, "Invalid or missing method", request_id=request.get("id"))
164 # Check ID for requests (not notifications)
165 if "id" in request:
166 request_id = request["id"]
167 if not isinstance(request_id, (str, int)) or isinstance(request_id, bool):
168 raise JSONRPCError(INVALID_REQUEST, "Invalid request ID type", request_id=None)
170 # Check params if present
171 params = request.get("params")
172 if params is not None:
173 if not isinstance(params, (dict, list)):
174 raise JSONRPCError(INVALID_REQUEST, "Invalid params type", request_id=request.get("id"))
177def validate_response(response: Dict[str, Any]) -> None:
178 """Validate JSON-RPC response.
180 Args:
181 response: Response dictionary to validate
183 Raises:
184 JSONRPCError: If response is invalid
186 Examples:
187 Valid success response:
188 >>> validate_response({"jsonrpc": "2.0", "result": 42, "id": 1})
190 Valid error response:
191 >>> validate_response({"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": 1})
193 Valid response with null result:
194 >>> validate_response({"jsonrpc": "2.0", "result": None, "id": 1})
196 Valid response with null id (for errors during id parsing):
197 >>> validate_response({"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": None})
199 Invalid version:
200 >>> validate_response({"jsonrpc": "1.0", "result": 42, "id": 1}) # doctest: +ELLIPSIS
201 Traceback (most recent call last):
202 ...
203 mcpgateway.validation.jsonrpc.JSONRPCError: Invalid JSON-RPC version
205 Missing ID:
206 >>> validate_response({"jsonrpc": "2.0", "result": 42}) # doctest: +ELLIPSIS
207 Traceback (most recent call last):
208 ...
209 mcpgateway.validation.jsonrpc.JSONRPCError: Missing response ID
211 Invalid ID type (boolean):
212 >>> validate_response({"jsonrpc": "2.0", "result": 42, "id": True}) # doctest: +ELLIPSIS
213 Traceback (most recent call last):
214 ...
215 mcpgateway.validation.jsonrpc.JSONRPCError: Invalid response ID type
217 Invalid ID type (list):
218 >>> validate_response({"jsonrpc": "2.0", "result": 42, "id": [1, 2]}) # doctest: +ELLIPSIS
219 Traceback (most recent call last):
220 ...
221 mcpgateway.validation.jsonrpc.JSONRPCError: Invalid response ID type
223 Missing both result and error:
224 >>> validate_response({"jsonrpc": "2.0", "id": 1}) # doctest: +ELLIPSIS
225 Traceback (most recent call last):
226 ...
227 mcpgateway.validation.jsonrpc.JSONRPCError: Response must contain either result or error
229 Both result and error present:
230 >>> validate_response({"jsonrpc": "2.0", "result": 42, "error": {"code": -1, "message": "Error"}, "id": 1}) # doctest: +ELLIPSIS
231 Traceback (most recent call last):
232 ...
233 mcpgateway.validation.jsonrpc.JSONRPCError: Response cannot contain both result and error
235 Invalid error object type:
236 >>> validate_response({"jsonrpc": "2.0", "error": "Invalid error", "id": 1}) # doctest: +ELLIPSIS
237 Traceback (most recent call last):
238 ...
239 mcpgateway.validation.jsonrpc.JSONRPCError: Invalid error object type
241 Error missing code:
242 >>> validate_response({"jsonrpc": "2.0", "error": {"message": "Error"}, "id": 1}) # doctest: +ELLIPSIS
243 Traceback (most recent call last):
244 ...
245 mcpgateway.validation.jsonrpc.JSONRPCError: Error must contain code and message
247 Error missing message:
248 >>> validate_response({"jsonrpc": "2.0", "error": {"code": -32601}, "id": 1}) # doctest: +ELLIPSIS
249 Traceback (most recent call last):
250 ...
251 mcpgateway.validation.jsonrpc.JSONRPCError: Error must contain code and message
253 Invalid error code type:
254 >>> validate_response({"jsonrpc": "2.0", "error": {"code": "invalid", "message": "Error"}, "id": 1}) # doctest: +ELLIPSIS
255 Traceback (most recent call last):
256 ...
257 mcpgateway.validation.jsonrpc.JSONRPCError: Error code must be integer
259 Invalid error message type:
260 >>> validate_response({"jsonrpc": "2.0", "error": {"code": -32601, "message": 123}, "id": 1}) # doctest: +ELLIPSIS
261 Traceback (most recent call last):
262 ...
263 mcpgateway.validation.jsonrpc.JSONRPCError: Error message must be string
265 Valid error with additional data:
266 >>> validate_response({"jsonrpc": "2.0", "error": {"code": -32602, "message": "Invalid params", "data": {"param": "name"}}, "id": 1})
267 """
268 # Check jsonrpc version
269 if response.get("jsonrpc") != "2.0":
270 raise JSONRPCError(INVALID_REQUEST, "Invalid JSON-RPC version", request_id=response.get("id"))
272 # Check ID
273 if "id" not in response:
274 raise JSONRPCError(INVALID_REQUEST, "Missing response ID", request_id=None)
276 response_id = response["id"]
277 if not isinstance(response_id, (str, int, type(None))) or isinstance(response_id, bool):
278 raise JSONRPCError(INVALID_REQUEST, "Invalid response ID type", request_id=None)
280 # Check result XOR error
281 has_result = "result" in response
282 has_error = "error" in response
284 if not has_result and not has_error:
285 raise JSONRPCError(INVALID_REQUEST, "Response must contain either result or error", request_id=id)
286 if has_result and has_error:
287 raise JSONRPCError(INVALID_REQUEST, "Response cannot contain both result and error", request_id=id)
289 # Validate error object
290 if has_error:
291 error = response["error"]
292 if not isinstance(error, dict):
293 raise JSONRPCError(INVALID_REQUEST, "Invalid error object type", request_id=id)
295 if "code" not in error or "message" not in error:
296 raise JSONRPCError(INVALID_REQUEST, "Error must contain code and message", request_id=id)
298 if not isinstance(error["code"], int):
299 raise JSONRPCError(INVALID_REQUEST, "Error code must be integer", request_id=id)
301 if not isinstance(error["message"], str):
302 raise JSONRPCError(INVALID_REQUEST, "Error message must be string", request_id=id)