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

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 

18from typing import Any, Dict, Optional, Union 

19 

20 

21class JSONRPCError(Exception): 

22 """JSON-RPC protocol error.""" 

23 

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. 

32 

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) 

44 

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

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

47 

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 

54 

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

56 

57 

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 

66 

67 

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

69 """Validate JSON-RPC request. 

70 

71 Args: 

72 request: Request dictionary to validate 

73 

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")) 

80 

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")) 

85 

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) 

91 

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")) 

97 

98 

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

100 """Validate JSON-RPC response. 

101 

102 Args: 

103 response: Response dictionary to validate 

104 

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")) 

111 

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) 

115 

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) 

119 

120 # Check result XOR error 

121 has_result = "result" in response 

122 has_error = "error" in response 

123 

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) 

128 

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) 

134 

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) 

137 

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) 

140 

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)