Coverage for src/qdrant_loader_mcp_server/mcp/protocol.py: 95%
66 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-04 05:45 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-04 05:45 +0000
1"""MCP Protocol implementation."""
3from typing import Any
6class MCPProtocol:
7 """MCP Protocol implementation for handling RAG requests."""
9 def __init__(self):
10 """Initialize MCP Protocol."""
11 self.version = "2.0"
12 self.initialized = False
14 def validate_request(self, request: dict[str, Any]) -> bool:
15 """Validate MCP request format according to JSON-RPC 2.0 specification.
17 Args:
18 request: The request to validate
20 Returns:
21 bool: True if request is valid, False otherwise
22 """
23 # Check for required fields
24 if not isinstance(request, dict):
25 return False
27 # Handle empty dict
28 if not request:
29 # Allow empty dict only during initialization
30 return not self.initialized
32 # For initialization request, be more lenient
33 if not self.initialized:
34 if request.get("method") == "initialize":
35 return True
37 # Standard validation for other requests
38 if "jsonrpc" not in request or request["jsonrpc"] != "2.0":
39 return False
41 if "method" not in request or not isinstance(request["method"], str):
42 return False
44 # For requests (not notifications), id is required
45 if "id" in request:
46 if not isinstance(request["id"], str | int):
47 return False
48 if request["id"] is None:
49 return False
50 else:
51 # This is a notification, which is valid
52 return True
54 # Params is optional but must be object or array if present
55 if "params" in request and not isinstance(request["params"], dict | list):
56 return False
58 return True
60 def validate_response(self, response: dict[str, Any]) -> bool:
61 """Validate MCP response format according to JSON-RPC 2.0 specification.
63 Args:
64 response: The response to validate
66 Returns:
67 bool: True if response is valid, False otherwise
68 """
69 if not isinstance(response, dict):
70 return False
72 # Empty response is valid for notifications
73 if not response:
74 return True
76 # Check required fields
77 if "jsonrpc" not in response or response["jsonrpc"] != "2.0":
78 return False
80 if "id" not in response:
81 return False
83 if not isinstance(response["id"], str | int):
84 return False
86 # Must have either result or error, but not both
87 has_result = "result" in response
88 has_error = "error" in response
90 if not has_result and not has_error:
91 return False
93 if has_result and has_error:
94 return False
96 # Validate error object structure if present
97 if has_error:
98 error = response["error"]
99 if not isinstance(error, dict):
100 return False
101 if "code" not in error or not isinstance(error["code"], int):
102 return False
103 if "message" not in error or not isinstance(error["message"], str):
104 return False
106 return True
108 def create_response(
109 self,
110 request_id: str | int | None,
111 result: Any | None = None,
112 error: dict | None = None,
113 ) -> dict[str, Any]:
114 """Create MCP response according to JSON-RPC 2.0 specification.
116 Args:
117 request_id: The ID of the request (None for notifications)
118 result: The result of the request
119 error: Any error that occurred
121 Returns:
122 Dict[str, Any]: The response object
123 """
124 # For notifications, return empty dict
125 if request_id is None:
126 return {}
128 # Create base response
129 response = {"jsonrpc": self.version, "id": request_id}
131 # Add either result or error, but not both
132 if error is not None:
133 if (
134 not isinstance(error, dict)
135 or "code" not in error
136 or "message" not in error
137 ):
138 error = {
139 "code": -32603,
140 "message": "Internal error",
141 "data": "Invalid error object format",
142 }
143 response["error"] = error
144 else:
145 # For successful responses, always include result (can be None)
146 response["result"] = result
148 # Validate response before returning
149 if not self.validate_response(response):
150 return {
151 "jsonrpc": self.version,
152 "id": request_id,
153 "error": {
154 "code": -32603,
155 "message": "Internal error",
156 "data": "Generated invalid response format",
157 },
158 }
160 return response
162 def mark_initialized(self):
163 """Mark the protocol as initialized."""
164 self.initialized = True