Coverage for mcpgateway/validation/jsonrpc.py: 80%
58 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-22 12:53 +0100
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-22 12:53 +0100
1# -*- coding: utf-8 -*-
2"""JSON-RPC Validation.
4Copyright 2025
5SPDX-License-Identifier: Apache-2.0
6Authors: Mihai Criveti
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
16"""
18from typing import Any, Dict, Optional, Union
21class JSONRPCError(Exception):
22 """JSON-RPC protocol error."""
24 def __init__(
25 self,
26 code: int,
27 message: str,
28 data: Optional[Any] = None,
29 request_id: Optional[Union[str, int]] = None,
30 ):
31 """Initialize JSON-RPC error.
33 Args:
34 code: Error code
35 message: Error message
36 data: Optional error data
37 request_id: Optional request ID
38 """
39 self.code = code
40 self.message = message
41 self.data = data
42 self.request_id = request_id
43 super().__init__(message)
45 def to_dict(self) -> Dict[str, Any]:
46 """Convert error to JSON-RPC error response dict.
48 Returns:
49 Error response dictionary
50 """
51 error = {"code": self.code, "message": self.message}
52 if self.data is not None:
53 error["data"] = self.data
55 return {"jsonrpc": "2.0", "error": error, "request_id": self.request_id}
58# Standard JSON-RPC error codes
59PARSE_ERROR = -32700 # Invalid JSON
60INVALID_REQUEST = -32600 # Invalid Request object
61METHOD_NOT_FOUND = -32601 # Method not found
62INVALID_PARAMS = -32602 # Invalid method parameters
63INTERNAL_ERROR = -32603 # Internal JSON-RPC error
64SERVER_ERROR_START = -32000 # Start of server error codes
65SERVER_ERROR_END = -32099 # End of server error codes
68def validate_request(request: Dict[str, Any]) -> None:
69 """Validate JSON-RPC request.
71 Args:
72 request: Request dictionary to validate
74 Raises:
75 JSONRPCError: If request is invalid
76 """
77 # Check jsonrpc version
78 if request.get("jsonrpc") != "2.0":
79 raise JSONRPCError(INVALID_REQUEST, "Invalid JSON-RPC version", request_id=request.get("id"))
81 # Check method
82 method = request.get("method")
83 if not isinstance(method, str) or not method:
84 raise JSONRPCError(INVALID_REQUEST, "Invalid or missing method", request_id=request.get("id"))
86 # Check ID for requests (not notifications)
87 if "id" in request:
88 request_id = request["id"]
89 if not isinstance(request_id, (str, int)) or isinstance(request_id, bool):
90 raise JSONRPCError(INVALID_REQUEST, "Invalid request ID type", request_id=None)
92 # Check params if present
93 params = request.get("params")
94 if params is not None:
95 if not isinstance(params, (dict, list)):
96 raise JSONRPCError(INVALID_REQUEST, "Invalid params type", request_id=request.get("id"))
99def validate_response(response: Dict[str, Any]) -> None:
100 """Validate JSON-RPC response.
102 Args:
103 response: Response dictionary to validate
105 Raises:
106 JSONRPCError: If response is invalid
107 """
108 # Check jsonrpc version
109 if response.get("jsonrpc") != "2.0": 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 raise JSONRPCError(INVALID_REQUEST, "Invalid JSON-RPC version", request_id=response.get("id"))
112 # Check ID
113 if "id" not in response: 113 ↛ 114line 113 didn't jump to line 114 because the condition on line 113 was never true
114 raise JSONRPCError(INVALID_REQUEST, "Missing response ID", request_id=None)
116 response_id = response["id"]
117 if not isinstance(response_id, (str, int, type(None))) or isinstance(response_id, bool): 117 ↛ 118line 117 didn't jump to line 118 because the condition on line 117 was never true
118 raise JSONRPCError(INVALID_REQUEST, "Invalid response ID type", request_id=None)
120 # Check result XOR error
121 has_result = "result" in response
122 has_error = "error" in response
124 if not has_result and not has_error: 124 ↛ 125line 124 didn't jump to line 125 because the condition on line 124 was never true
125 raise JSONRPCError(INVALID_REQUEST, "Response must contain either result or error", request_id=id)
126 if has_result and has_error: 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true
127 raise JSONRPCError(INVALID_REQUEST, "Response cannot contain both result and error", request_id=id)
129 # Validate error object
130 if has_error:
131 error = response["error"]
132 if not isinstance(error, dict): 132 ↛ 133line 132 didn't jump to line 133 because the condition on line 132 was never true
133 raise JSONRPCError(INVALID_REQUEST, "Invalid error object type", request_id=id)
135 if "code" not in error or "message" not in error: 135 ↛ 136line 135 didn't jump to line 136 because the condition on line 135 was never true
136 raise JSONRPCError(INVALID_REQUEST, "Error must contain code and message", request_id=id)
138 if not isinstance(error["code"], int): 138 ↛ 139line 138 didn't jump to line 139 because the condition on line 138 was never true
139 raise JSONRPCError(INVALID_REQUEST, "Error code must be integer", request_id=id)
141 if not isinstance(error["message"], str): 141 ↛ 142line 141 didn't jump to line 142 because the condition on line 141 was never true
142 raise JSONRPCError(INVALID_REQUEST, "Error message must be string", request_id=id)