Skip to main content
Glama
test_mcp_server.py9.12 kB
import sys from pathlib import Path import pytest import pytest_asyncio from ipybox.mcp_client import MCPClient from tests.integration.mcp_server import STDIO_SERVER_PATH MCP_SERVER_NAME = "test_mcp" @pytest_asyncio.fixture async def mcp_client(tmp_path: Path): """Create an MCPClient connected to the ipybox MCP server.""" # Create .env file with KERNEL_ENV_ prefixed variable for testing dotenv_file = tmp_path / ".env" dotenv_file.write_text("KERNEL_ENV_TEST_VAR=test_value_from_dotenv\n") server_params = { "command": sys.executable, "args": ["-m", "ipybox.mcp_server", "--workspace", str(tmp_path), "--log-level", "ERROR"], } async with MCPClient(server_params, connect_timeout=30) as client: yield client class TestBasicExecution: """Basic MCP server functionality.""" @pytest.mark.asyncio async def test_simple_code_execution(self, mcp_client: MCPClient): """Test executing a simple print statement.""" result = await mcp_client.run("execute_ipython_cell", {"code": "print('hello world')"}) assert result == "hello world" @pytest.mark.asyncio async def test_expression_result(self, mcp_client: MCPClient): """Test that expression results are returned.""" result = await mcp_client.run("execute_ipython_cell", {"code": "2 + 2"}) assert result == "4" @pytest.mark.asyncio async def test_code_execution_error(self, mcp_client: MCPClient): """Test that execution errors are raised.""" with pytest.raises(Exception) as exc_info: await mcp_client.run("execute_ipython_cell", {"code": "raise ValueError('test error')"}) assert "ValueError" in str(exc_info.value) assert "test error" in str(exc_info.value) @pytest.mark.asyncio async def test_state_persistence(self, mcp_client: MCPClient): """Test that kernel state persists across executions.""" await mcp_client.run("execute_ipython_cell", {"code": "x = 42"}) result = await mcp_client.run("execute_ipython_cell", {"code": "print(x)"}) assert result == "42" @pytest.mark.asyncio async def test_max_output_chars_truncation(self, mcp_client: MCPClient): """Test that output is truncated when exceeding max_output_chars.""" # Generate output longer than the limit code = "print('x' * 100)" result = await mcp_client.run("execute_ipython_cell", {"code": code, "max_output_chars": 50}) assert isinstance(result, str) assert len(result) > 50 # Includes truncation message assert result.startswith("x" * 50) assert "[Output truncated: exceeded 50 character limit]" in result @pytest.mark.asyncio async def test_max_output_chars_no_truncation(self, mcp_client: MCPClient): """Test that output is not truncated when within max_output_chars.""" code = "print('hello world')" result = await mcp_client.run("execute_ipython_cell", {"code": code, "max_output_chars": 100}) assert isinstance(result, str) assert result == "hello world" assert "[Output truncated" not in result @pytest.mark.asyncio async def test_max_output_chars_default(self, mcp_client: MCPClient): """Test that default max_output_chars (5000) is used when not specified.""" # Generate output slightly over 5000 chars code = "print('x' * 5001)" result = await mcp_client.run("execute_ipython_cell", {"code": code}) assert isinstance(result, str) assert "[Output truncated: exceeded 5000 character limit]" in result assert result.startswith("x" * 5000) @pytest.mark.asyncio async def test_dotenv_kernel_env_var_available(self, mcp_client: MCPClient): """Test that KERNEL_ENV_ variables from .env are available in kernel.""" code = "import os; print(os.environ.get('TEST_VAR', 'NOT_FOUND'))" result = await mcp_client.run("execute_ipython_cell", {"code": code}) assert result == "test_value_from_dotenv" class TestMcpServerRegistration: """MCP server registration tests.""" @pytest.mark.asyncio async def test_register_mcp_server_returns_tool_names(self, mcp_client: MCPClient): """Test that register_mcp_server returns tool names.""" server_params = { "command": "python", "args": [str(STDIO_SERVER_PATH)], } result = await mcp_client.run( "register_mcp_server", {"server_name": MCP_SERVER_NAME, "server_params": server_params}, ) assert isinstance(result, str) tool_names = result.split("\n") assert "tool_1" in tool_names assert "tool_2" in tool_names assert "tool_3" in tool_names @pytest.mark.asyncio async def test_registered_tools_generate_sources(self, mcp_client: MCPClient, tmp_path: Path): """Test that registration generates importable sources in the workspace.""" server_params = { "command": "python", "args": [str(STDIO_SERVER_PATH)], } await mcp_client.run( "register_mcp_server", {"server_name": MCP_SERVER_NAME, "server_params": server_params}, ) # Verify the package was generated package_dir = tmp_path / "mcptools" / MCP_SERVER_NAME assert package_dir.exists() assert (package_dir / "__init__.py").exists() assert (package_dir / "tool_1.py").exists() assert (package_dir / "tool_2.py").exists() assert (package_dir / "tool_3.py").exists() @pytest.mark.asyncio async def test_registered_tools_are_callable(self, mcp_client: MCPClient): """Test that registered tools can be imported and called via execute_ipython_cell.""" server_params = { "command": "python", "args": [str(STDIO_SERVER_PATH)], } await mcp_client.run( "register_mcp_server", {"server_name": MCP_SERVER_NAME, "server_params": server_params}, ) # Sources are generated at mcptools/{server_name}/ code = f""" from mcptools.{MCP_SERVER_NAME}.tool_2 import run, Params result = run(Params(s="hello")) print(result) """ result = await mcp_client.run("execute_ipython_cell", {"code": code}) assert isinstance(result, str) assert "You passed to tool 2: hello" in result class TestReset: """Kernel reset tests.""" @pytest.mark.asyncio async def test_reset_clears_kernel_state(self, mcp_client: MCPClient): """Test that reset clears kernel state.""" # Set a variable await mcp_client.run("execute_ipython_cell", {"code": "x = 42"}) # Verify it exists result = await mcp_client.run("execute_ipython_cell", {"code": "print(x)"}) assert result == "42" # Reset await mcp_client.run("reset", {}) # Verify the variable no longer exists with pytest.raises(Exception) as exc_info: await mcp_client.run("execute_ipython_cell", {"code": "print(x)"}) assert "NameError" in str(exc_info.value) @pytest.mark.asyncio async def test_reset_allows_continued_execution(self, mcp_client: MCPClient): """Test that reset allows continued execution.""" # Set state and reset await mcp_client.run("execute_ipython_cell", {"code": "x = 42"}) await mcp_client.run("reset", {}) # Verify we can still execute code result = await mcp_client.run("execute_ipython_cell", {"code": "print('after reset')"}) assert result == "after reset" @pytest.mark.skipif(sys.platform != "darwin", reason="Sandbox tests only run on macOS") class TestSandbox: """Tests for sandbox configuration.""" HTTP_CODE = """ import urllib.request response = urllib.request.urlopen('https://example.org') content = response.read().decode('utf-8') print(content) """ @pytest_asyncio.fixture async def mcp_client_custom_sandbox(self, tmp_path: Path): """Create an MCPClient with custom sandbox config (example.org allowed).""" sandbox_config = Path("tests", "integration", "sandbox.json").absolute() server_params = { "command": sys.executable, "args": [ "-m", "ipybox.mcp_server", "--workspace", str(tmp_path), "--log-level", "ERROR", "--sandbox", "--sandbox-config", str(sandbox_config), ], } async with MCPClient(server_params, connect_timeout=30) as client: yield client @pytest.mark.asyncio async def test_custom_sandbox_allows_example_org(self, mcp_client_custom_sandbox: MCPClient): """Test that custom sandbox config allows example.org access.""" result = await mcp_client_custom_sandbox.run("execute_ipython_cell", {"code": self.HTTP_CODE}) assert result is not None assert "Example Domain" in result

Latest Blog Posts

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/gradion-ai/ipybox'

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