Skip to main content
Glama
jbroll

MCP Build Environment Service

by jbroll
test_integration.py14.4 kB
""" Integration tests for MCP Build Service These tests run against a live MCP server instance to verify: - Server startup and initialization - All tool implementations - Error handling - Multi-repository support """ import asyncio import os import pytest import pytest_asyncio from pathlib import Path import tempfile import subprocess from helpers.mcp_client import MCPClient # Test environment TEST_REPOS_DIR = Path(tempfile.gettempdir()) / "mcp-build-test-repos" @pytest.fixture(scope="session") def test_repos_dir(): """Create a test repository directory structure""" # Clean up any existing test directory if TEST_REPOS_DIR.exists(): import shutil shutil.rmtree(TEST_REPOS_DIR) TEST_REPOS_DIR.mkdir(parents=True, exist_ok=True) # Create test repo 1 with a Makefile repo1 = TEST_REPOS_DIR / "test-repo-1" repo1.mkdir() subprocess.run(["git", "init"], cwd=repo1, check=True, capture_output=True) subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=repo1, check=True) subprocess.run(["git", "config", "user.name", "Test User"], cwd=repo1, check=True) # Disable commit signing for tests subprocess.run(["git", "config", "commit.gpgsign", "false"], cwd=repo1, check=True) subprocess.run(["git", "config", "gpg.format", "openpgp"], cwd=repo1, check=True) # Create a simple Makefile (with actual tabs) makefile = repo1 / "Makefile" makefile.write_text(".PHONY: all clean test\n\n" + "all:\n" + "\t@echo \"Building project...\"\n" + "\t@echo \"Build complete\"\n\n" + "clean:\n" + "\t@echo \"Cleaning build artifacts...\"\n" + "\t@rm -f *.o\n\n" + "test:\n" + "\t@echo \"Running tests...\"\n" + "\t@echo \"All tests passed\"\n") # Create a test file and commit test_file = repo1 / "test.txt" test_file.write_text("Hello from test repo 1") subprocess.run(["git", "add", "."], cwd=repo1, check=True) subprocess.run(["git", "commit", "-m", "Initial commit"], cwd=repo1, check=True) # Create test repo 2 repo2 = TEST_REPOS_DIR / "test-repo-2" repo2.mkdir() subprocess.run(["git", "init"], cwd=repo2, check=True, capture_output=True) subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=repo2, check=True) subprocess.run(["git", "config", "user.name", "Test User"], cwd=repo2, check=True) # Disable commit signing for tests subprocess.run(["git", "config", "commit.gpgsign", "false"], cwd=repo2, check=True) subprocess.run(["git", "config", "gpg.format", "openpgp"], cwd=repo2, check=True) test_file2 = repo2 / "README.md" test_file2.write_text("# Test Repo 2") subprocess.run(["git", "add", "."], cwd=repo2, check=True) subprocess.run(["git", "commit", "-m", "Initial commit"], cwd=repo2, check=True) yield TEST_REPOS_DIR # Cleanup import shutil if TEST_REPOS_DIR.exists(): shutil.rmtree(TEST_REPOS_DIR) @pytest_asyncio.fixture async def mcp_client(test_repos_dir): """Create and initialize an MCP client""" client = MCPClient( ["python", "-m", "server"], cwd=str(test_repos_dir) ) await client.start() await client.initialize() yield client await client.stop() @pytest.mark.asyncio async def test_server_initialization(test_repos_dir): """Test that the server initializes correctly""" client = MCPClient( ["python", "-m", "server"], cwd=str(test_repos_dir) ) try: await client.start() result = await client.initialize() # Check that we got a valid response assert "protocolVersion" in result assert "serverInfo" in result assert result["serverInfo"]["name"] == "mcp-build" # Check capabilities assert "capabilities" in result finally: await client.stop() @pytest.mark.asyncio async def test_list_tools(mcp_client): """Test listing available tools""" tools = await mcp_client.list_tools() # Check that all expected tools are present tool_names = {tool["name"] for tool in tools} expected_tools = {"list", "make", "git", "ls", "env", "read_file"} assert expected_tools.issubset(tool_names), f"Missing tools: {expected_tools - tool_names}" # Check that each tool has required fields for tool in tools: assert "name" in tool assert "description" in tool assert "inputSchema" in tool @pytest.mark.asyncio async def test_list_repositories(mcp_client): """Test listing repositories""" result = await mcp_client.call_tool("list") assert len(result) == 1 content = result[0] assert content["type"] == "text" text = content["text"] assert "test-repo-1" in text assert "test-repo-2" in text @pytest.mark.asyncio async def test_git_status(mcp_client): """Test git status command""" result = await mcp_client.call_tool("git", {"args": "status", "repo": "test-repo-1"}) assert len(result) == 1 content = result[0] assert content["type"] == "text" text = content["text"] # Should contain git status output assert "branch" in text.lower() or "nothing to commit" in text.lower() @pytest.mark.asyncio async def test_git_log(mcp_client): """Test git log command""" result = await mcp_client.call_tool("git", {"args": "log --oneline -5", "repo": "test-repo-1"}) assert len(result) == 1 content = result[0] assert content["type"] == "text" text = content["text"] # Should contain commit history assert "Initial commit" in text @pytest.mark.asyncio async def test_git_branch(mcp_client): """Test git branch command""" result = await mcp_client.call_tool("git", {"args": "branch", "repo": "test-repo-1"}) assert len(result) == 1 content = result[0] assert content["type"] == "text" text = content["text"] # Should show at least the main/master branch assert "main" in text or "master" in text @pytest.mark.asyncio async def test_git_invalid_command(mcp_client): """Test that invalid git commands are rejected""" result = await mcp_client.call_tool("git", {"args": "push origin main", "repo": "test-repo-1"}) assert len(result) == 1 text = result[0]["text"] assert "Error:" in text and "not allowed" in text @pytest.mark.asyncio async def test_make_all(mcp_client): """Test make all command""" result = await mcp_client.call_tool("make", {"args": "all", "repo": "test-repo-1"}) assert len(result) == 1 content = result[0] assert content["type"] == "text" text = content["text"] # Should contain make output assert "Building project" in text or "Build complete" in text @pytest.mark.asyncio async def test_make_test(mcp_client): """Test make test command""" result = await mcp_client.call_tool("make", {"args": "test", "repo": "test-repo-1"}) assert len(result) == 1 content = result[0] assert content["type"] == "text" text = content["text"] # Should contain test output assert "Running tests" in text or "All tests passed" in text @pytest.mark.asyncio async def test_make_clean(mcp_client): """Test make clean command""" result = await mcp_client.call_tool("make", {"args": "clean", "repo": "test-repo-1"}) assert len(result) == 1 content = result[0] assert content["type"] == "text" text = content["text"] # Should contain clean output assert "Cleaning" in text @pytest.mark.asyncio async def test_make_no_args(mcp_client): """Test make without arguments (runs default target)""" result = await mcp_client.call_tool("make", {"repo": "test-repo-1"}) assert len(result) == 1 content = result[0] assert content["type"] == "text" @pytest.mark.asyncio async def test_ls_basic(mcp_client): """Test ls command""" result = await mcp_client.call_tool("ls", {"repo": "test-repo-1"}) assert len(result) == 1 content = result[0] assert content["type"] == "text" text = content["text"] # Should list files in the repository assert "Makefile" in text or "test.txt" in text @pytest.mark.asyncio async def test_ls_with_flags(mcp_client): """Test ls with flags""" result = await mcp_client.call_tool("ls", {"args": "-la", "repo": "test-repo-1"}) assert len(result) == 1 content = result[0] assert content["type"] == "text" text = content["text"] # Should include hidden files with -a flag assert ".git" in text @pytest.mark.asyncio async def test_ls_specific_path(mcp_client): """Test ls with specific path""" result = await mcp_client.call_tool("ls", {"args": "-l .git", "repo": "test-repo-1"}) assert len(result) == 1 content = result[0] assert content["type"] == "text" text = content["text"] # Should list .git directory contents assert "config" in text or "HEAD" in text @pytest.mark.asyncio async def test_env_command(mcp_client): """Test env command""" result = await mcp_client.call_tool("env", {"repo": "test-repo-1"}) assert len(result) == 1 content = result[0] assert content["type"] == "text" text = content["text"] # Should contain environment information assert "PATH" in text or "python" in text.lower() @pytest.mark.asyncio async def test_multi_repo_list(mcp_client): """Test that both repositories are discovered""" result = await mcp_client.call_tool("list") content = result[0] text = content["text"] # Both repos should be listed assert "test-repo-1" in text assert "test-repo-2" in text @pytest.mark.asyncio async def test_multi_repo_git_status(mcp_client): """Test git status on specific repository""" result = await mcp_client.call_tool("git", { "args": "status", "repo": "test-repo-2" }) assert len(result) == 1 content = result[0] assert content["type"] == "text" @pytest.mark.asyncio async def test_multi_repo_ls(mcp_client): """Test ls on specific repository""" result = await mcp_client.call_tool("ls", { "args": "-l", "repo": "test-repo-2" }) assert len(result) == 1 content = result[0] text = content["text"] # Should list files from repo 2 assert "README.md" in text @pytest.mark.asyncio async def test_invalid_repo_name(mcp_client): """Test that invalid repository names are rejected""" result = await mcp_client.call_tool("git", { "args": "status", "repo": "nonexistent-repo" }) assert len(result) == 1 text = result[0]["text"] assert "Error:" in text and "Unknown repository" in text @pytest.mark.asyncio async def test_dangerous_path_traversal(mcp_client): """Test that path traversal attempts are blocked""" result = await mcp_client.call_tool("ls", {"args": "../../../etc/passwd", "repo": "test-repo-1"}) assert len(result) == 1 text = result[0]["text"] assert "Error:" in text and ("dangerous patterns" in text or "traversal" in text) @pytest.mark.asyncio async def test_dangerous_command_injection(mcp_client): """Test that command injection attempts are blocked""" result = await mcp_client.call_tool("make", {"args": "all; rm -rf /", "repo": "test-repo-1"}) assert len(result) == 1 text = result[0]["text"] assert "Error:" in text and "dangerous patterns" in text @pytest.mark.asyncio async def test_concurrent_tool_calls(mcp_client): """Test that multiple tool calls work sequentially""" # Run multiple commands sequentially (MCP stdio doesn't support true concurrency) result1 = await mcp_client.call_tool("list") result2 = await mcp_client.call_tool("git", {"args": "status", "repo": "test-repo-1"}) result3 = await mcp_client.call_tool("ls", {"args": "-l", "repo": "test-repo-1"}) # All should complete successfully assert len(result1) == 1 and result1[0]["type"] == "text" assert len(result2) == 1 and result2[0]["type"] == "text" assert len(result3) == 1 and result3[0]["type"] == "text" @pytest.mark.asyncio async def test_error_handling_empty_git_args(mcp_client): """Test error handling for empty git arguments""" result = await mcp_client.call_tool("git", {"args": "", "repo": "test-repo-1"}) assert len(result) == 1 text = result[0]["text"] assert "Error:" in text @pytest.mark.asyncio async def test_long_running_command(mcp_client): """Test handling of commands that produce output""" # Git log can produce significant output result = await mcp_client.call_tool("git", {"args": "log --all --oneline", "repo": "test-repo-1"}) assert len(result) == 1 content = result[0] assert content["type"] == "text" # Should have received all output assert len(content["text"]) > 0 @pytest.mark.asyncio async def test_read_file_basic(mcp_client, test_repos_dir): """Test reading a file""" result = await mcp_client.call_tool("read_file", { "repo": "test-repo-1", "path": "test.txt" }) assert len(result) == 1 content = result[0] assert content["type"] == "text" text = content["text"] assert "Hello from test repo 1" in text @pytest.mark.asyncio async def test_read_file_with_line_range(mcp_client, test_repos_dir): """Test reading a file with line range""" # Read the Makefile with a line range result = await mcp_client.call_tool("read_file", { "repo": "test-repo-1", "path": "Makefile", "start_line": 1, "end_line": 5 }) assert len(result) == 1 content = result[0] assert content["type"] == "text" text = content["text"] # Should show line range info assert "Lines 1-5" in text @pytest.mark.asyncio async def test_read_file_security(mcp_client): """Test that file reading is restricted to repository""" result = await mcp_client.call_tool("read_file", { "repo": "test-repo-1", "path": "/etc/passwd" }) assert len(result) == 1 text = result[0]["text"] assert "Error:" in text and "outside repository" in text if __name__ == "__main__": pytest.main([__file__, "-v", "-s"])

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/jbroll/mcp-build'

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