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
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-09 11:03 +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"""
18# Standard
19from typing import Any, Dict, Optional, Union
22class JSONRPCError(Exception):
23 """JSON-RPC protocol error."""
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.
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)
46 def to_dict(self) -> Dict[str, Any]:
47 """Convert error to JSON-RPC error response dict.
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
56 return {"jsonrpc": "2.0", "error": error, "request_id": self.request_id}
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
69def validate_request(request: Dict[str, Any]) -> None:
70 """Validate JSON-RPC request.
72 Args:
73 request: Request dictionary to validate
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"))
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"))
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)
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"))
100def validate_response(response: Dict[str, Any]) -> None:
101 """Validate JSON-RPC response.
103 Args:
104 response: Response dictionary to validate
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"))
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)
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)
121 # Check result XOR error
122 has_result = "result" in response
123 has_error = "error" in response
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)
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)
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)
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)
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)