Coverage for mcpgateway/validation/jsonrpc.py: 80%

58 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-09 11:03 +0100

1# -*- coding: utf-8 -*- 

2"""JSON-RPC Validation. 

3 

4Copyright 2025 

5SPDX-License-Identifier: Apache-2.0 

6Authors: Mihai Criveti 

7 

8This module provides validation functions for JSON-RPC 2.0 requests and responses 

9according to the specification at https://www.jsonrpc.org/specification. 

10 

11Includes: 

12- Request validation 

13- Response validation 

14- Standard error codes 

15- Error message formatting 

16""" 

17 

18# Standard 

19from typing import Any, Dict, Optional, Union 

20 

21 

22class JSONRPCError(Exception): 

23 """JSON-RPC protocol error.""" 

24 

25 def __init__( 

26 self, 

27 code: int, 

28 message: str, 

29 data: Optional[Any] = None, 

30 request_id: Optional[Union[str, int]] = None, 

31 ): 

32 """Initialize JSON-RPC error. 

33 

34 Args: 

35 code: Error code 

36 message: Error message 

37 data: Optional error data 

38 request_id: Optional request ID 

39 """ 

40 self.code = code 

41 self.message = message 

42 self.data = data 

43 self.request_id = request_id 

44 super().__init__(message) 

45 

46 def to_dict(self) -> Dict[str, Any]: 

47 """Convert error to JSON-RPC error response dict. 

48 

49 Returns: 

50 Error response dictionary 

51 """ 

52 error = {"code": self.code, "message": self.message} 

53 if self.data is not None: 

54 error["data"] = self.data 

55 

56 return {"jsonrpc": "2.0", "error": error, "request_id": self.request_id} 

57 

58 

59# Standard JSON-RPC error codes 

60PARSE_ERROR = -32700 # Invalid JSON 

61INVALID_REQUEST = -32600 # Invalid Request object 

62METHOD_NOT_FOUND = -32601 # Method not found 

63INVALID_PARAMS = -32602 # Invalid method parameters 

64INTERNAL_ERROR = -32603 # Internal JSON-RPC error 

65SERVER_ERROR_START = -32000 # Start of server error codes 

66SERVER_ERROR_END = -32099 # End of server error codes 

67 

68 

69def validate_request(request: Dict[str, Any]) -> None: 

70 """Validate JSON-RPC request. 

71 

72 Args: 

73 request: Request dictionary to validate 

74 

75 Raises: 

76 JSONRPCError: If request is invalid 

77 """ 

78 # Check jsonrpc version 

79 if request.get("jsonrpc") != "2.0": 

80 raise JSONRPCError(INVALID_REQUEST, "Invalid JSON-RPC version", request_id=request.get("id")) 

81 

82 # Check method 

83 method = request.get("method") 

84 if not isinstance(method, str) or not method: 

85 raise JSONRPCError(INVALID_REQUEST, "Invalid or missing method", request_id=request.get("id")) 

86 

87 # Check ID for requests (not notifications) 

88 if "id" in request: 

89 request_id = request["id"] 

90 if not isinstance(request_id, (str, int)) or isinstance(request_id, bool): 

91 raise JSONRPCError(INVALID_REQUEST, "Invalid request ID type", request_id=None) 

92 

93 # Check params if present 

94 params = request.get("params") 

95 if params is not None: 

96 if not isinstance(params, (dict, list)): 

97 raise JSONRPCError(INVALID_REQUEST, "Invalid params type", request_id=request.get("id")) 

98 

99 

100def validate_response(response: Dict[str, Any]) -> None: 

101 """Validate JSON-RPC response. 

102 

103 Args: 

104 response: Response dictionary to validate 

105 

106 Raises: 

107 JSONRPCError: If response is invalid 

108 """ 

109 # Check jsonrpc version 

110 if response.get("jsonrpc") != "2.0": 110 ↛ 111line 110 didn't jump to line 111 because the condition on line 110 was never true

111 raise JSONRPCError(INVALID_REQUEST, "Invalid JSON-RPC version", request_id=response.get("id")) 

112 

113 # Check ID 

114 if "id" not in response: 114 ↛ 115line 114 didn't jump to line 115 because the condition on line 114 was never true

115 raise JSONRPCError(INVALID_REQUEST, "Missing response ID", request_id=None) 

116 

117 response_id = response["id"] 

118 if not isinstance(response_id, (str, int, type(None))) or isinstance(response_id, bool): 118 ↛ 119line 118 didn't jump to line 119 because the condition on line 118 was never true

119 raise JSONRPCError(INVALID_REQUEST, "Invalid response ID type", request_id=None) 

120 

121 # Check result XOR error 

122 has_result = "result" in response 

123 has_error = "error" in response 

124 

125 if not has_result and not has_error: 125 ↛ 126line 125 didn't jump to line 126 because the condition on line 125 was never true

126 raise JSONRPCError(INVALID_REQUEST, "Response must contain either result or error", request_id=id) 

127 if has_result and has_error: 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true

128 raise JSONRPCError(INVALID_REQUEST, "Response cannot contain both result and error", request_id=id) 

129 

130 # Validate error object 

131 if has_error: 

132 error = response["error"] 

133 if not isinstance(error, dict): 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true

134 raise JSONRPCError(INVALID_REQUEST, "Invalid error object type", request_id=id) 

135 

136 if "code" not in error or "message" not in error: 136 ↛ 137line 136 didn't jump to line 137 because the condition on line 136 was never true

137 raise JSONRPCError(INVALID_REQUEST, "Error must contain code and message", request_id=id) 

138 

139 if not isinstance(error["code"], int): 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true

140 raise JSONRPCError(INVALID_REQUEST, "Error code must be integer", request_id=id) 

141 

142 if not isinstance(error["message"], str): 142 ↛ 143line 142 didn't jump to line 143 because the condition on line 142 was never true

143 raise JSONRPCError(INVALID_REQUEST, "Error message must be string", request_id=id)