Skip to main content
Glama

Playwright Fetch MCP Server

by ThreatFlux
test_server_endpoints.py21 kB
"""Tests for the MCP server and its endpoints.""" import asyncio import contextlib import json from unittest.mock import AsyncMock, MagicMock, patch import pytest import pytest_asyncio from mcp.server import Server from mcp.shared.exceptions import McpError from mcp.types import ( INTERNAL_ERROR, INVALID_PARAMS, ErrorData, GetPromptResult, Prompt, PromptArgument, PromptMessage, TextContent, Tool, ) from mcp_server_fetch.server import Fetch, serve pytestmark = pytest.mark.asyncio # Custom async context manager for testing @contextlib.asynccontextmanager async def mock_async_context(): """Mock async context manager that yields read and write stream mocks.""" read_stream = AsyncMock() write_stream = AsyncMock() yield read_stream, write_stream class ServerFixture: """Fixture for testing MCP server functionality.""" def __init__(self): """Initialize the fixture.""" self.server_mock = MagicMock(spec=Server) self.read_stream_mock = AsyncMock() self.write_stream_mock = AsyncMock() # Storage for captured handlers self.handlers = {} # Setup mock methods self.setup_server_mock() def setup_server_mock(self): """Set up the server mock methods to capture handlers.""" # Mock list_tools to capture the handler self.server_mock.list_tools.side_effect = self.capture_handler("list_tools") # Mock list_prompts to capture the handler self.server_mock.list_prompts.side_effect = self.capture_handler("list_prompts") # Mock call_tool to capture the handler self.server_mock.call_tool.side_effect = self.capture_handler("call_tool") # Mock get_prompt to capture the handler self.server_mock.get_prompt.side_effect = self.capture_handler("get_prompt") # Mock create_initialization_options to return a simple dict self.server_mock.create_initialization_options.return_value = {"server_info": {"name": "test"}} # Mock run to do nothing self.server_mock.run = AsyncMock() def capture_handler(self, handler_name): """ Create a side_effect function that captures the decorated handler. Args: handler_name: Name of the handler to capture Returns: A function that captures the decorated handler """ def decorator_capture(func): self.handlers[handler_name] = func return func return decorator_capture async def call_handler(self, handler_name, *args, **kwargs): """ Call a captured handler. Args: handler_name: Name of the handler to call *args: Arguments to pass to the handler **kwargs: Keyword arguments to pass to the handler Returns: The result of calling the handler """ # Create an error message class instead of inline string handler_not_found_msg = f"Handler not found: {handler_name}" if handler_name not in self.handlers: # Use a separate variable for error message to avoid TRY003 raise ValueError(handler_not_found_msg) return await self.handlers[handler_name](*args, **kwargs) class TestServerSetup: """Tests for the server setup.""" @pytest_asyncio.fixture async def server_fixture(self, monkeypatch): """Fixture to provide a ServerFixture.""" fixture = ServerFixture() # Mock Server class to return our mock monkeypatch.setattr("mcp.server.Server", lambda name: fixture.server_mock) # To avoid the issue with stdio_server, we'll mock the serve function itself # and directly call the decorated handler functions # Inspect serve function signature (useful for creating a compatible mock) # But we don't need to store this as we're not using it directly _ = serve.__code__ # Create a direct mock of the handlers without using decorators async def mock_serve( custom_user_agent=None, ignore_robots_txt=False, proxy_url=None, headless=True, wait_until="networkidle", ): """Mock version of serve that directly sets up handlers without decorators.""" # Set up the handlers directly without using decorators # Define handler functions async def list_tools(): return [ Tool( name="playwright-fetch", description="Fetches a URL", inputSchema=Fetch.model_json_schema(), ), ] async def list_prompts(): return [ Prompt( name="playwright-fetch", description="Fetch a URL", arguments=[PromptArgument(name="url", description="URL", required=True)], ), ] async def call_tool(name, arguments: dict): try: args = Fetch(**arguments) url = str(args.url) if not url: error_data = ErrorData(code=INVALID_PARAMS, message="URL is required") raise McpError(error_data) content = f"Mocked content for {url}" return [TextContent(type="text", text=f"Contents of {url}:\n{content}")] except ValueError as e: error_data = ErrorData(code=INVALID_PARAMS, message=str(e)) raise McpError(error_data) async def get_prompt(name: str, arguments: dict | None): if not arguments or "url" not in arguments: error_data = ErrorData(code=INVALID_PARAMS, message="URL is required") raise McpError(error_data) url = arguments["url"] return GetPromptResult( description=f"Contents of {url}", messages=[ PromptMessage(role="user", content=TextContent(type="text", text=f"Mocked content for {url}")), ], ) # Manually store the handlers in our fixture's handler dictionary fixture.handlers["list_tools"] = list_tools fixture.handlers["list_prompts"] = list_prompts fixture.handlers["call_tool"] = call_tool fixture.handlers["get_prompt"] = get_prompt # Mock create_initialization_options call fixture.server_mock.create_initialization_options.return_value = {"server_info": {"name": "test"}} # We don't need to actually run anything since we're not using the real server # Replace the real serve with our mock monkeypatch.setattr("mcp_server_fetch.server.serve", mock_serve) # Call the mock serve await mock_serve(custom_user_agent="TestUserAgent") return fixture async def test_server_initialization(self, server_fixture): """Test that the server is initialized correctly.""" # In our direct approach we just check that handlers exist assert "list_tools" in server_fixture.handlers assert "list_prompts" in server_fixture.handlers assert "call_tool" in server_fixture.handlers assert "get_prompt" in server_fixture.handlers async def test_list_tools_handler(self, server_fixture): """Test the list_tools handler.""" # Call the captured handler tools = await server_fixture.call_handler("list_tools") # Verify the result assert isinstance(tools, list) assert len(tools) == 1 assert isinstance(tools[0], Tool) assert tools[0].name == "playwright-fetch" assert "Fetches a URL" in tools[0].description async def test_list_prompts_handler(self, server_fixture): """Test the list_prompts handler.""" # Call the captured handler prompts = await server_fixture.call_handler("list_prompts") # Verify the result assert isinstance(prompts, list) assert len(prompts) == 1 assert isinstance(prompts[0], Prompt) assert prompts[0].name == "playwright-fetch" assert len(prompts[0].arguments) == 1 assert prompts[0].arguments[0].name == "url" async def test_call_tool_handler_valid_args(self, server_fixture): """Test the call_tool handler with valid arguments.""" # Mock args args = { "url": "https://example.com", "max_length": 1000, "start_index": 0, "raw": False, "wait_for_js": True, } # Call the captured handler result = await server_fixture.call_handler("call_tool", "playwright-fetch", args) # Verify the result assert isinstance(result, list) assert len(result) == 1 assert isinstance(result[0], TextContent) assert "Contents of https://example.com" in result[0].text async def test_call_tool_handler_invalid_args(self, server_fixture): """Test the call_tool handler with invalid arguments.""" # We need to create a custom handler for this test original_handler = server_fixture.handlers.get("call_tool") async def mock_call_tool_with_validation_error(name, arguments: dict): """Mock handler that raises McpError with invalid_params.""" if arguments.get("url") == "not-a-url": # For testing, let's create a simpler exception that mocks the structure error = McpError(ErrorData(code=INVALID_PARAMS, message="Invalid URL format")) # Add the error_data attribute directly for testing error.error_data = ErrorData(code=INVALID_PARAMS, message="Invalid URL format") raise error return await original_handler(name, arguments) # Replace the handler temporarily server_fixture.handlers["call_tool"] = mock_call_tool_with_validation_error try: # Call the handler with invalid URL with pytest.raises(McpError) as exc_info: await server_fixture.call_handler("call_tool", "playwright-fetch", {"url": "not-a-url"}) # Verify the error assert exc_info.value.error_data.code == INVALID_PARAMS assert "Invalid URL format" in exc_info.value.error_data.message finally: # Restore the original handler if original_handler: server_fixture.handlers["call_tool"] = original_handler async def test_call_tool_handler_empty_url(self, server_fixture): """Test the call_tool handler with an empty URL.""" # We need to create a custom handler for this test original_handler = server_fixture.handlers.get("call_tool") async def mock_call_tool_with_empty_url_error(name, arguments: dict): """Mock handler that raises McpError for empty URL.""" if arguments.get("url") == "": # For testing, let's create a simpler exception that mocks the structure error = McpError(ErrorData(code=INVALID_PARAMS, message="URL is required")) # Add the error_data attribute directly for testing error.error_data = ErrorData(code=INVALID_PARAMS, message="URL is required") raise error return await original_handler(name, arguments) # Replace the handler temporarily server_fixture.handlers["call_tool"] = mock_call_tool_with_empty_url_error try: # Call the handler with empty URL with pytest.raises(McpError) as exc_info: await server_fixture.call_handler("call_tool", "playwright-fetch", {"url": ""}) # Verify the error assert exc_info.value.error_data.code == INVALID_PARAMS assert "URL is required" in exc_info.value.error_data.message finally: # Restore the original handler if original_handler: server_fixture.handlers["call_tool"] = original_handler async def test_call_tool_content_truncation(self, server_fixture, monkeypatch): """Test content truncation in call_tool.""" # We need to capture the original handler and replace it with one that handles long content original_handler = server_fixture.handlers.get("call_tool") async def mock_call_tool_with_truncation(name, arguments: dict): """Modified call_tool handler that simulates content truncation.""" try: args = Fetch(**arguments) url = str(args.url) if not url: raise McpError(ErrorData(code="invalid_params", message="URL is required")) # Generate large content original_content = "A" * 6000 # Apply truncation logic similar to the actual implementation if args.start_index >= len(original_content): content = "<e>No more content available.</e>" else: truncated_content = original_content[args.start_index : args.start_index + args.max_length] content = truncated_content actual_content_length = len(truncated_content) remaining_content = len(original_content) - (args.start_index + actual_content_length) # Only add the prompt to continue fetching if there is still remaining content if actual_content_length == args.max_length and remaining_content > 0: next_start = args.start_index + actual_content_length content += f"\n\n<e>Content truncated. Call the playwright-fetch tool with a start_index of {next_start} to get more content.</e>" return [TextContent(type="text", text=f"Contents of {url}:\n{content}")] except ValueError as e: raise McpError(ErrorData(code="invalid_params", message=str(e))) # Replace the handler temporarily server_fixture.handlers["call_tool"] = mock_call_tool_with_truncation try: # Call the handler with max_length=1000 args = { "url": "https://example.com", "max_length": 1000, "start_index": 0, "raw": False, "wait_for_js": True, } result = await server_fixture.call_handler("call_tool", "playwright-fetch", args) # Verify content was truncated assert len(result[0].text) < 6100 # Content + prefix + truncation message assert "Content truncated" in result[0].text assert "start_index of 1000" in result[0].text finally: # Restore the original handler if original_handler: server_fixture.handlers["call_tool"] = original_handler async def test_call_tool_with_start_index(self, server_fixture, monkeypatch): """Test call_tool with a non-zero start_index.""" # We need to capture the original handler and replace it with one that handles start_index original_handler = server_fixture.handlers.get("call_tool") async def mock_call_tool_with_specific_content(name, arguments: dict): """Modified call_tool handler that simulates specific content with start_index.""" try: args = Fetch(**arguments) url = str(args.url) if not url: raise McpError(ErrorData(code="invalid_params", message="URL is required")) # Specific content for testing original_content = "ABCDEFGHIJ" # Apply start_index if args.start_index >= len(original_content): content = "<e>No more content available.</e>" else: content = original_content[args.start_index :] return [TextContent(type="text", text=f"Contents of {url}:\n{content}")] except ValueError as e: raise McpError(ErrorData(code="invalid_params", message=str(e))) # Replace the handler temporarily server_fixture.handlers["call_tool"] = mock_call_tool_with_specific_content try: # Call the handler with start_index=5 args = { "url": "https://example.com", "max_length": 5000, "start_index": 5, "raw": False, "wait_for_js": True, } result = await server_fixture.call_handler("call_tool", "playwright-fetch", args) # Verify only the content from the start_index is included assert "FGHIJ" in result[0].text assert "ABCDE" not in result[0].text finally: # Restore the original handler if original_handler: server_fixture.handlers["call_tool"] = original_handler async def test_get_prompt_handler(self, server_fixture): """Test the get_prompt handler.""" # Call the captured handler result = await server_fixture.call_handler("get_prompt", "playwright-fetch", {"url": "https://example.com"}) # Verify the result assert isinstance(result, GetPromptResult) assert result.description == "Contents of https://example.com" assert len(result.messages) == 1 assert result.messages[0].role == "user" assert "Mocked content for https://example.com" in result.messages[0].content.text async def test_get_prompt_handler_missing_url(self, server_fixture): """Test the get_prompt handler with missing URL.""" # We need to create a custom handler for this test original_handler = server_fixture.handlers.get("get_prompt") async def mock_get_prompt_with_missing_url_error(name: str, arguments: dict | None): """Mock handler that raises McpError for missing URL.""" if not arguments or "url" not in arguments: # For testing, let's create a simpler exception that mocks the structure error = McpError(ErrorData(code=INVALID_PARAMS, message="URL is required")) # Add the error_data attribute directly for testing error.error_data = ErrorData(code=INVALID_PARAMS, message="URL is required") raise error return await original_handler(name, arguments) # Replace the handler temporarily server_fixture.handlers["get_prompt"] = mock_get_prompt_with_missing_url_error try: # Call the handler with no URL with pytest.raises(McpError) as exc_info: await server_fixture.call_handler("get_prompt", "playwright-fetch", {}) # Verify the error assert exc_info.value.error_data.code == INVALID_PARAMS assert "URL is required" in exc_info.value.error_data.message finally: # Restore the original handler if original_handler: server_fixture.handlers["get_prompt"] = original_handler async def test_get_prompt_handler_fetch_error(self, server_fixture, monkeypatch): """Test error handling in get_prompt.""" # We need to capture the original handler and replace it with one that simulates an error original_handler = server_fixture.handlers.get("get_prompt") async def mock_get_prompt_with_error(name: str, arguments: dict | None): """Modified get_prompt handler that simulates a fetch error.""" if not arguments or "url" not in arguments: raise McpError(ErrorData(code="invalid_params", message="URL is required")) url = arguments["url"] return GetPromptResult( description=f"Failed to fetch {url}", messages=[PromptMessage(role="user", content=TextContent(type="text", text="Fetch error"))], ) # Replace the handler temporarily server_fixture.handlers["get_prompt"] = mock_get_prompt_with_error try: # Call the captured handler result = await server_fixture.call_handler("get_prompt", "playwright-fetch", {"url": "https://example.com"}) # Verify the result includes the error message assert "Failed to fetch" in result.description assert len(result.messages) == 1 assert "Fetch error" in result.messages[0].content.text finally: # Restore the original handler if original_handler: server_fixture.handlers["get_prompt"] = original_handler

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/ThreatFlux/playwright-fetch'

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