BREAKING CHANGES: - Package renamed from mcp-arduino-server to mcp-arduino - Command changed to 'mcp-arduino' (was 'mcp-arduino-server') - Repository moved to git.supported.systems/MCP/mcp-arduino NEW FEATURES: ✨ Smart client capability detection and dual-mode sampling support ✨ Intelligent WireViz templates with component-specific circuits (LED, motor, sensor, button, display) ✨ Client debug tools for MCP capability inspection ✨ Enhanced error handling with progressive enhancement patterns IMPROVEMENTS: 🧹 Major repository cleanup - removed 14+ experimental files and tests 📝 Consolidated and reorganized documentation 🐛 Fixed import issues and applied comprehensive linting with ruff 📦 Updated author information to Ryan Malloy (ryan@supported.systems) 🔧 Fixed package version references in startup code TECHNICAL DETAILS: - Added dual-mode WireViz: AI generation for sampling clients, smart templates for others - Implemented client capability detection via MCP handshake inspection - Created progressive enhancement pattern for universal MCP client compatibility - Organized test files into proper structure (tests/examples/) - Applied comprehensive code formatting and lint fixes The server now provides excellent functionality for ALL MCP clients regardless of their sampling capabilities, while preserving advanced features for clients that support them. Version: 2025.09.27.1
204 lines
7.8 KiB
Python
204 lines
7.8 KiB
Python
"""
|
|
Integration tests for the Arduino MCP Server (cleaned version)
|
|
|
|
These tests verify server architecture and component integration
|
|
without requiring full MCP protocol simulation.
|
|
"""
|
|
|
|
|
|
import pytest
|
|
|
|
from src.mcp_arduino_server.config import ArduinoServerConfig
|
|
from src.mcp_arduino_server.server_refactored import create_server
|
|
|
|
|
|
class TestServerIntegration:
|
|
"""Test suite for server architecture integration"""
|
|
|
|
@pytest.fixture
|
|
def test_config(self, tmp_path):
|
|
"""Create test configuration with temporary directories"""
|
|
return ArduinoServerConfig(
|
|
arduino_cli_path="/usr/bin/arduino-cli",
|
|
sketches_base_dir=tmp_path / "sketches",
|
|
build_temp_dir=tmp_path / "build",
|
|
wireviz_path="/usr/bin/wireviz",
|
|
command_timeout=30,
|
|
enable_client_sampling=True
|
|
)
|
|
|
|
@pytest.fixture
|
|
def mcp_server(self, test_config):
|
|
"""Create a test MCP server instance"""
|
|
return create_server(test_config)
|
|
|
|
def test_server_creation(self, test_config):
|
|
"""Test that server creates successfully with all components"""
|
|
server = create_server(test_config)
|
|
|
|
assert server is not None
|
|
assert server.name == "Arduino Development Server"
|
|
|
|
# Verify directories were created
|
|
assert test_config.sketches_base_dir.exists()
|
|
assert test_config.build_temp_dir.exists()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_server_tools_registration(self, mcp_server):
|
|
"""Test that all expected tools are registered"""
|
|
# Get all registered tools
|
|
tools = await mcp_server.get_tools()
|
|
tool_names = list(tools.keys())
|
|
|
|
# Verify sketch tools
|
|
sketch_tools = [name for name in tool_names if name.startswith('arduino_') and 'sketch' in name]
|
|
assert len(sketch_tools) >= 5 # create, list, read, write, compile, upload
|
|
|
|
# Verify library tools
|
|
library_tools = [name for name in tool_names if 'librar' in name]
|
|
assert len(library_tools) >= 3 # search, install, list examples
|
|
|
|
# Verify board tools
|
|
board_tools = [name for name in tool_names if 'board' in name or 'core' in name]
|
|
assert len(board_tools) >= 4 # list boards, search boards, install core, list cores
|
|
|
|
# Verify debug tools
|
|
debug_tools = [name for name in tool_names if 'debug' in name]
|
|
assert len(debug_tools) >= 5 # start, interactive, break, run, print, etc.
|
|
|
|
# Verify WireViz tools
|
|
wireviz_tools = [name for name in tool_names if 'wireviz' in name]
|
|
assert len(wireviz_tools) >= 2 # generate from yaml, generate from description
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_server_resources_registration(self, mcp_server):
|
|
"""Test that all expected resources are registered"""
|
|
# Get all registered resources
|
|
resources = await mcp_server.get_resources()
|
|
resource_uris = list(resources.keys())
|
|
|
|
expected_resources = [
|
|
"arduino://sketches",
|
|
"arduino://libraries",
|
|
"arduino://boards",
|
|
"arduino://debug/sessions",
|
|
"wireviz://instructions",
|
|
"server://info"
|
|
]
|
|
|
|
for expected_uri in expected_resources:
|
|
assert expected_uri in resource_uris, f"Resource {expected_uri} not found in {resource_uris}"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_server_info_resource(self, mcp_server):
|
|
"""Test the server info resource provides correct information"""
|
|
# Get the server info resource
|
|
info_resource = await mcp_server.get_resource("server://info")
|
|
info_content = await info_resource.read()
|
|
|
|
assert "Arduino Development Server" in info_content
|
|
assert "Configuration:" in info_content
|
|
assert "Components:" in info_content
|
|
assert "Available Tool Categories:" in info_content
|
|
assert "Sketch Tools:" in info_content
|
|
assert "Library Tools:" in info_content
|
|
assert "Board Tools:" in info_content
|
|
assert "Debug Tools:" in info_content
|
|
assert "WireViz Tools:" in info_content
|
|
|
|
def test_component_isolation(self, test_config):
|
|
"""Test that components can be created independently"""
|
|
from src.mcp_arduino_server.components import (
|
|
ArduinoBoard,
|
|
ArduinoDebug,
|
|
ArduinoLibrary,
|
|
ArduinoSketch,
|
|
WireViz,
|
|
)
|
|
|
|
# Each component should initialize without errors
|
|
sketch = ArduinoSketch(test_config)
|
|
library = ArduinoLibrary(test_config)
|
|
board = ArduinoBoard(test_config)
|
|
debug = ArduinoDebug(test_config)
|
|
wireviz = WireViz(test_config)
|
|
|
|
# Components should have expected attributes
|
|
assert hasattr(sketch, 'config')
|
|
assert hasattr(library, 'config')
|
|
assert hasattr(board, 'config')
|
|
assert hasattr(debug, 'config')
|
|
assert hasattr(wireviz, 'config')
|
|
|
|
def test_configuration_flexibility(self, tmp_path):
|
|
"""Test that server handles various configuration scenarios"""
|
|
# Test minimal configuration
|
|
minimal_config = ArduinoServerConfig(
|
|
sketches_base_dir=tmp_path / "minimal"
|
|
)
|
|
server1 = create_server(minimal_config)
|
|
assert server1 is not None
|
|
|
|
# Test custom configuration
|
|
custom_config = ArduinoServerConfig(
|
|
arduino_cli_path="/custom/arduino-cli",
|
|
wireviz_path="/custom/wireviz",
|
|
sketches_base_dir=tmp_path / "custom",
|
|
command_timeout=60,
|
|
enable_client_sampling=False
|
|
)
|
|
server2 = create_server(custom_config)
|
|
assert server2 is not None
|
|
|
|
# Test that different configs create distinct servers
|
|
assert server1 is not server2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tool_naming_consistency(self, mcp_server):
|
|
"""Test that tools follow consistent naming patterns"""
|
|
tools = await mcp_server.get_tools()
|
|
tool_names = list(tools.keys())
|
|
|
|
arduino_tools = [name for name in tool_names if name.startswith('arduino_')]
|
|
wireviz_tools = [name for name in tool_names if name.startswith('wireviz_')]
|
|
|
|
# Should have both Arduino and WireViz tools
|
|
assert len(arduino_tools) > 0, "No Arduino tools found"
|
|
assert len(wireviz_tools) > 0, "No WireViz tools found"
|
|
|
|
# Arduino tools should follow patterns
|
|
for tool_name in arduino_tools:
|
|
# Should have component_action pattern
|
|
parts = tool_name.split('_')
|
|
assert len(parts) >= 2, f"Arduino tool {tool_name} doesn't follow naming pattern"
|
|
assert parts[0] == 'arduino'
|
|
|
|
# WireViz tools should follow patterns
|
|
for tool_name in wireviz_tools:
|
|
parts = tool_name.split('_')
|
|
assert len(parts) >= 2, f"WireViz tool {tool_name} doesn't follow naming pattern"
|
|
assert parts[0] == 'wireviz'
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resource_uri_patterns(self, mcp_server):
|
|
"""Test that resource URIs follow expected patterns"""
|
|
resources = await mcp_server.get_resources()
|
|
|
|
# Group by scheme
|
|
schemes = {}
|
|
for uri in resources.keys():
|
|
scheme = str(uri).split('://')[0]
|
|
if scheme not in schemes:
|
|
schemes[scheme] = []
|
|
schemes[scheme].append(uri)
|
|
|
|
# Should have expected schemes
|
|
assert 'arduino' in schemes, "No arduino:// resources found"
|
|
assert 'wireviz' in schemes, "No wireviz:// resources found"
|
|
assert 'server' in schemes, "No server:// resources found"
|
|
|
|
# Each scheme should have reasonable number of resources
|
|
assert len(schemes['arduino']) >= 3, "Too few arduino:// resources"
|
|
assert len(schemes['wireviz']) >= 1, "Too few wireviz:// resources"
|
|
assert len(schemes['server']) >= 1, "Too few server:// resources"
|