Skip to main content
Glama

MCP Server for Odoo

test_transport_integration.py•13.8 kB
"""Transport integration tests for MCP server. These tests verify that both stdio and streamable-http transports work correctly with the Odoo MCP server in integration with pytest. """ import asyncio import json import subprocess import sys from typing import Any, Dict, List, Optional import pytest import requests from tests.helpers.mcp_test_client import MCPTestClient # Mark all tests in this module as integration tests requiring Odoo pytestmark = [pytest.mark.integration, pytest.mark.odoo_required] class HttpTransportTester: """Helper class for testing streamable-http transport.""" def __init__(self, base_url: str = "http://localhost:8002/mcp/"): self.base_url = base_url.rstrip("/") + "/" self.session = requests.Session() self.session_id: Optional[str] = None self.request_id = 0 self.server_process = None def _next_id(self) -> int: """Get next request ID.""" self.request_id += 1 return self.request_id @staticmethod def _parse_sse_response(text: str) -> List[Dict[str, Any]]: """Parse SSE-formatted response.""" results = [] for line in text.strip().split("\n"): if line.startswith("data:"): try: data = json.loads(line[5:].strip()) results.append(data) except json.JSONDecodeError: pass return results async def start_server(self, port: int = 8002, timeout: int = 10) -> bool: """Start HTTP server.""" try: self.server_process = subprocess.Popen( [ sys.executable, "-m", "mcp_server_odoo", "--transport", "streamable-http", "--port", str(port), ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # Wait for server to start await asyncio.sleep(5) # Test if server is responding try: requests.get(f"http://localhost:{port}/", timeout=3) return True except requests.exceptions.RequestException: # Server might not have a root endpoint, try the MCP endpoint try: # Just check if we can connect requests.post( f"http://localhost:{port}/mcp/", json={"test": "connectivity"}, timeout=3 ) return True except requests.exceptions.RequestException: return self.server_process.poll() is None except Exception as e: print(f"Failed to start HTTP server: {e}") return False def stop_server(self): """Stop HTTP server.""" if self.server_process: self.server_process.terminate() try: self.server_process.wait(timeout=5) except subprocess.TimeoutExpired: self.server_process.kill() self.server_process.wait() self.server_process = None async def _send_request( self, method: str, params: Dict[str, Any], request_id: Optional[int] = None ) -> Optional[Dict[str, Any]]: """Send HTTP request and return response.""" headers = { "Content-Type": "application/json", "Accept": "application/json, text/event-stream", } if self.session_id: headers["mcp-session-id"] = self.session_id payload = {"jsonrpc": "2.0", "method": method, "params": params} if request_id is not None: payload["id"] = request_id try: response = self.session.post(self.base_url, json=payload, headers=headers, timeout=15) # Update session ID if provided if "mcp-session-id" in response.headers: self.session_id = response.headers["mcp-session-id"] # Parse SSE response if response.status_code == 200: events = self._parse_sse_response(response.text) for event in events: if request_id is not None and event.get("id") == request_id: return event elif request_id is None: return {"status": response.status_code} return { "error": f"Request failed with status {response.status_code}", "status": response.status_code, } except Exception as e: return {"error": str(e)} class TestTransportIntegration: """Integration tests for MCP transports.""" @pytest.mark.asyncio async def test_stdio_transport_basic_flow(self, odoo_server_required): """Test stdio transport basic initialization and communication.""" client = MCPTestClient() try: async with client.connect(): # Test basic operations tools = await client.list_tools() assert len(tools) > 0, "Expected at least one tool" await client.list_resources() # Resources might be empty, that's ok for transport testing # Test a basic tool call - list_models should work with proper auth result = await client.call_tool("list_models", {}) assert result is not None, "Tool call should return a result" # The result should be a proper MCP response assert hasattr(result, "content"), "Tool result should have content" except Exception as e: # Log the actual error for debugging import logging logging.error(f"stdio transport test failed: {e}") # Re-raise to fail the test raise @pytest.mark.asyncio async def test_stdio_transport_multiple_requests(self, odoo_server_required): """Test stdio transport can handle multiple sequential requests.""" client = MCPTestClient() try: async with client.connect(): # Make multiple tool list requests for i in range(3): tools = await client.list_tools() assert len(tools) > 0, f"Expected tools on request {i + 1}" # Make multiple resource list requests for i in range(3): resources = await client.list_resources() # Resources might be empty, that's ok - just testing transport stability assert ( resources is not None ), f"Resource list should not be None on request {i + 1}" except Exception as e: # Log the actual error for debugging import logging logging.error(f"stdio multiple requests test failed: {e}") raise @pytest.mark.asyncio async def test_http_transport_basic_flow(self, odoo_server_required): """Test streamable-http transport basic initialization and communication.""" tester = HttpTransportTester() try: # Start server assert await tester.start_server(), "Failed to start HTTP server" # Test initialization init_params = { "protocolVersion": "2025-03-26", "capabilities": {"tools": {}, "resources": {}, "prompts": {}}, "clientInfo": {"name": "pytest-http", "version": "1.0.0"}, } response = await tester._send_request("initialize", init_params, tester._next_id()) assert response is not None, "No response to initialize request" assert "error" not in response, f"Error in initialize response: {response}" assert "result" in response, f"Expected result in response, got: {response}" assert tester.session_id is not None, "No session ID received" # Send initialized notification await tester._send_request("notifications/initialized", {}) # Test tools/list response = await tester._send_request("tools/list", {}, tester._next_id()) assert response is not None, "No response to tools/list request" assert "error" not in response, f"Error in tools/list response: {response}" assert "result" in response, f"Expected result in tools/list response, got: {response}" finally: tester.stop_server() @pytest.mark.asyncio async def test_http_transport_session_persistence(self, odoo_server_required): """Test that HTTP transport maintains session across requests.""" tester = HttpTransportTester() try: # Start and initialize server assert await tester.start_server(), "Failed to start HTTP server" # Initialize init_params = { "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {"name": "pytest-http", "version": "1.0"}, } response = await tester._send_request("initialize", init_params, tester._next_id()) assert response and "result" in response original_session_id = tester.session_id assert original_session_id is not None, "No session ID after initialize" # Send initialized notification await tester._send_request("notifications/initialized", {}) # Make multiple requests and verify session ID persists for i in range(3): response = await tester._send_request("tools/list", {}, tester._next_id()) assert response is not None, f"No response to request {i + 1}" assert "error" not in response, f"Error in request {i + 1}: {response}" assert ( tester.session_id == original_session_id ), f"Session ID changed on request {i + 1}" finally: tester.stop_server() @pytest.mark.asyncio async def test_http_transport_tool_call(self, odoo_server_required): """Test HTTP transport can execute tool calls.""" tester = HttpTransportTester() try: # Start and initialize server assert await tester.start_server(), "Failed to start HTTP server" # Initialize init_params = { "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {"name": "pytest-http", "version": "1.0"}, } response = await tester._send_request("initialize", init_params, tester._next_id()) assert response and "result" in response # Send initialized notification await tester._send_request("notifications/initialized", {}) # Test list_models tool call params = {"name": "list_models", "arguments": {}} response = await tester._send_request("tools/call", params, tester._next_id()) assert response is not None, "No response to tool call" # Note: The tool call might fail due to auth, but the transport should work # Just check that we got some kind of response (transport working) assert isinstance(response, dict), f"Expected dict response, got: {response}" # Accept either successful result or any error that's not a transport error has_result = "result" in response has_error = "error" in response if has_error: error = response.get("error") # If error is a dict with code, check it's not a transport error (-32600) if isinstance(error, dict) and error.get("code") == -32600: raise AssertionError(f"Transport error in tool call: {response}") # If we get here, either we have a result or a non-transport error assert has_result or has_error, f"Response should have result or error: {response}" finally: tester.stop_server() @pytest.mark.integration class TestTransportCompatibility: """Test transport compatibility and edge cases.""" @pytest.mark.asyncio async def test_server_version_consistency(self, odoo_server_required): """Test that both transports can successfully connect and communicate.""" # Test stdio connection stdio_client = MCPTestClient() stdio_connected = False try: async with stdio_client.connect(): # Test basic operation to verify connection works tools = await stdio_client.list_tools() stdio_connected = len(tools) > 0 except Exception: stdio_connected = False # Test HTTP connection http_tester = HttpTransportTester() http_connected = False try: if await http_tester.start_server(): response = await http_tester._send_request( "initialize", { "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {"name": "test", "version": "1.0"}, }, 1, ) http_connected = response is not None and "result" in response finally: http_tester.stop_server() # Both transports should successfully connect assert stdio_connected, "Failed to connect via stdio transport" assert http_connected, "Failed to connect via HTTP transport"

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AlejandroLaraPolanco/mcp-odoo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server