test_functional.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. #!/usr/bin/env python3
  2. """
  3. Functional testing of MCP server core components
  4. """
  5. import subprocess
  6. import time
  7. import json
  8. import sys
  9. import os
  10. import threading
  11. import queue
  12. class MCPServerTester:
  13. def __init__(self, server_path):
  14. self.server_path = server_path
  15. self.server_process = None
  16. self.message_queue = queue.Queue()
  17. def start_server(self):
  18. """Start MCP server process"""
  19. print(f"Starting MCP server: {self.server_path}")
  20. self.server_process = subprocess.Popen(
  21. [self.server_path],
  22. stdin=subprocess.PIPE,
  23. stdout=subprocess.PIPE,
  24. stderr=subprocess.PIPE,
  25. text=False,
  26. bufsize=0
  27. )
  28. # Start thread to read stderr
  29. stderr_thread = threading.Thread(target=self._read_stderr)
  30. stderr_thread.daemon = True
  31. stderr_thread.start()
  32. # Start thread to read stdout
  33. stdout_thread = threading.Thread(target=self._read_stdout)
  34. stdout_thread.daemon = True
  35. stdout_thread.start()
  36. time.sleep(1)
  37. def _read_stderr(self):
  38. """Read stderr output from server"""
  39. while self.server_process and self.server_process.poll() is None:
  40. try:
  41. line = self.server_process.stderr.readline()
  42. if line:
  43. print(f"SERVER STDERR: {line.decode().strip()}")
  44. except:
  45. break
  46. def _read_stdout(self):
  47. """Read stdout output from server without Content-Length parsing"""
  48. buffer = b""
  49. while self.server_process and self.server_process.poll() is None:
  50. try:
  51. data = self.server_process.stdout.readline()
  52. if not data:
  53. break
  54. buffer += data
  55. # Try to parse JSON directly from each line
  56. try:
  57. message = buffer.strip()
  58. if message:
  59. parsed = json.loads(message.decode())
  60. self.message_queue.put(parsed)
  61. buffer = b""
  62. except json.JSONDecodeError:
  63. # If not valid JSON yet, continue reading
  64. continue
  65. except:
  66. break
  67. def send_jsonrpc_request(self, method, params=None, id=1):
  68. """Send a JSON-RPC request without Content-Length header"""
  69. request = {
  70. "jsonrpc": "2.0",
  71. "method": method,
  72. "id": id,
  73. "params": params or {}
  74. }
  75. request_str = json.dumps(request)
  76. message = f"{request_str}\n"
  77. self.server_process.stdin.write(message.encode())
  78. self.server_process.stdin.flush()
  79. try:
  80. response = self.message_queue.get(timeout=10)
  81. return response
  82. except queue.Empty:
  83. print("TIMEOUT: No response received")
  84. return None
  85. def test_server_initialization(self):
  86. """Test server initialization and lifecycle"""
  87. print("\n=== Testing Server Initialization ===")
  88. # Test initialize request
  89. init_params = {
  90. "protocolVersion": "2025-11-25",
  91. "capabilities": {},
  92. "clientInfo": {
  93. "name": "test-client",
  94. "version": "1.0.0"
  95. }
  96. }
  97. print("\nSending initialize request...")
  98. init_response = self.send_jsonrpc_request("initialize", init_params, id=1)
  99. if init_response and "result" in init_response:
  100. result = init_response["result"]
  101. # Check protocol version
  102. if result.get("protocolVersion") == "2025-11-25":
  103. print("✓ Protocol version correct")
  104. else:
  105. print("✗ Protocol version incorrect")
  106. # Check capabilities
  107. if "capabilities" in result:
  108. caps = result["capabilities"]
  109. if "resources" in caps and "tools" in caps and "prompts" in caps:
  110. print("✓ Capabilities structure correct")
  111. else:
  112. print("✗ Capabilities structure incorrect")
  113. else:
  114. print("✗ Capabilities missing")
  115. # Check server info
  116. if "serverInfo" in result:
  117. info = result["serverInfo"]
  118. if "name" in info and "version" in info:
  119. print("✓ Server info correct")
  120. else:
  121. print("✗ Server info incorrect")
  122. else:
  123. print("✗ Server info missing")
  124. return True
  125. else:
  126. print("✗ Initialize response invalid")
  127. return False
  128. def test_jsonrpc_message_handling(self):
  129. """Test JSON-RPC message handling with GLib.Variant"""
  130. print("\n=== Testing JSON-RPC Message Handling ===")
  131. # Test valid JSON-RPC request
  132. print("\nTesting valid JSON-RPC request...")
  133. ping_response = self.send_jsonrpc_request("ping", {}, id=2)
  134. if ping_response and "result" in ping_response and ping_response.get("id") == 2:
  135. print("✓ Valid JSON-RPC request handled correctly")
  136. else:
  137. print("✗ Valid JSON-RPC request failed")
  138. # Test request with complex params
  139. print("\nTesting request with complex parameters...")
  140. complex_params = {
  141. "nested": {
  142. "array": [1, 2, 3],
  143. "object": {"key": "value"},
  144. "boolean": True,
  145. "number": 42.5,
  146. "null": None
  147. }
  148. }
  149. complex_response = self.send_jsonrpc_request("ping", complex_params, id=3)
  150. if complex_response and "result" in complex_response:
  151. print("✓ Complex parameters handled correctly")
  152. else:
  153. print("✗ Complex parameters failed")
  154. # Test invalid method
  155. print("\nTesting invalid method...")
  156. invalid_response = self.send_jsonrpc_request("invalid_method", {}, id=4)
  157. if invalid_response and "error" in invalid_response:
  158. print("✓ Invalid method handled correctly")
  159. else:
  160. print("✗ Invalid method not handled properly")
  161. def test_error_handling(self):
  162. """Test error handling throughout the system"""
  163. print("\n=== Testing Error Handling ===")
  164. # Test malformed JSON (this would need to be sent at a lower level)
  165. print("\nTesting error responses...")
  166. # Test missing required parameters
  167. print("\nTesting missing required parameters...")
  168. # This would depend on specific method implementations
  169. # Test invalid parameter types
  170. print("\nTesting invalid parameter types...")
  171. # This would depend on specific method implementations
  172. print("✓ Error handling tests completed")
  173. def stop_server(self):
  174. """Stop MCP server process"""
  175. if self.server_process:
  176. print("\nStopping server...")
  177. self.server_process.terminate()
  178. try:
  179. self.server_process.wait(timeout=5)
  180. except subprocess.TimeoutExpired:
  181. self.server_process.kill()
  182. self.server_process = None
  183. def main():
  184. if len(sys.argv) != 2:
  185. print("Usage: python3 test_functional.py <server-executable>")
  186. sys.exit(1)
  187. server_path = sys.argv[1]
  188. if not os.path.exists(server_path):
  189. print(f"Error: Server executable '{server_path}' not found")
  190. sys.exit(1)
  191. tester = MCPServerTester(server_path)
  192. try:
  193. tester.start_server()
  194. time.sleep(2)
  195. # Run functional tests
  196. init_success = tester.test_server_initialization()
  197. if init_success:
  198. tester.test_jsonrpc_message_handling()
  199. tester.test_error_handling()
  200. print("\n✓ All functional tests completed successfully")
  201. else:
  202. print("\n✗ Functional tests failed - initialization error")
  203. except KeyboardInterrupt:
  204. print("\nTest interrupted by user")
  205. except Exception as e:
  206. print(f"\nError during test: {e}")
  207. import traceback
  208. traceback.print_exc()
  209. finally:
  210. tester.stop_server()
  211. if __name__ == "__main__":
  212. main()