Skip to main content
Glama
FASTMCP_PREVENTION_STRATEGIES.md19.4 kB
# FastMCP Prevention Strategies & Test Patterns Based on the FastMCP migration of mcp-browser-use, this document captures lessons learned and prevention strategies to avoid future issues when working with FastMCP. ## 1. Best Practices for FastMCP Development ### 1.1 Correct Dependency Injection Patterns #### CurrentContext Usage **CORRECT: Use `CurrentContext()` for tool parameters** ```python from fastmcp import FastMCP from fastmcp.dependencies import CurrentContext from fastmcp.server.context import Context server = FastMCP("my_server") @server.tool() async def my_tool( task: str, ctx: Context = CurrentContext(), # ✅ Correct - uses CurrentContext() ) -> str: """Tool that needs context.""" # ctx is automatically injected by FastMCP runtime return f"Processing: {task}" ``` **INCORRECT: Using `Context()` directly as default** ```python from fastmcp.server.context import Context @server.tool() async def my_tool( task: str, ctx: Context = Context(), # ❌ WRONG - creates static instance ) -> str: """Tool with broken context injection.""" # ctx is None or stale - not properly injected return f"Processing: {task}" ``` **Why it matters:** - `CurrentContext()` is a sentinel that FastMCP recognizes to inject the actual request context at runtime - `Context()` creates a static instance that won't be updated for each request - This breaks any context-dependent operations (authentication, request metadata, etc.) #### Progress Usage **CORRECT: Use `Progress()` with optional checking** ```python from fastmcp.dependencies import Progress from typing import Optional @server.tool(task=TaskConfig(mode="optional")) async def long_running_task( topic: str, progress: Progress = Progress(), # ✅ Correct - uses Progress() ) -> str: """Tool with progress reporting.""" # Check if progress is available before using if progress: await progress.set_total(100) await progress.set_message("Starting task...") # Do work... if progress: await progress.increment() return "Complete" ``` **INCORRECT: Using None as default** ```python from fastmcp.dependencies import Progress from typing import Optional @server.tool(task=TaskConfig(mode="optional")) async def long_running_task( topic: str, progress: Optional[Progress] = None, # ❌ WRONG - defeats FastMCP's dependency system ) -> str: """Tool with broken progress.""" # FastMCP won't inject Progress if you use Optional[Progress] = None # You're managing the fallback yourself, which is fragile if progress: await progress.set_total(100) return "Complete" ``` **Why it matters:** - FastMCP's `Progress()` default automatically handles the "optional" case - Using `Optional[Progress] = None` bypasses FastMCP's dependency injection system - The tool may work in synchronous mode but fail in background task mode #### Helper Pattern for Conditional Progress ```python from fastmcp.dependencies import Progress class ResearchMachine: def __init__(self, topic: str, progress: Progress = Progress()): self.topic = topic self.progress = progress async def _report_progress( self, message: Optional[str] = None, increment: bool = False, total: Optional[int] = None, ) -> None: """Report progress only if available. This pattern allows graceful degradation: - Works with background task mode (Progress is injected) - Works with synchronous mode (Progress is empty, operations are no-ops) """ if not self.progress: return if total is not None: await self.progress.set_total(total) if message: await self.progress.set_message(message) if increment: await self.progress.increment() ``` ### 1.2 Task Configuration Patterns **CORRECT: Optional background task mode** ```python from fastmcp import FastMCP, TaskConfig @server.tool(task=TaskConfig(mode="optional")) async def my_research_tool( topic: str, ctx: Context = CurrentContext(), progress: Progress = Progress(), ) -> str: """ Tool that supports both synchronous and background execution. - If client requests task=true: runs in background, progress updates stream - If client requests task=false: runs synchronously, returns result directly """ return "Report" ``` **Required task mode (for truly background-only operations):** ```python @server.tool(task=TaskConfig(mode="required")) async def background_only_task( topic: str, progress: Progress = Progress(), ) -> str: """This tool MUST be executed as a background task.""" return "Report" ``` ### 1.3 Import Organization **CORRECT: Import structure for FastMCP tools** ```python # ✅ Correct import pattern from fastmcp import FastMCP, TaskConfig from fastmcp.dependencies import CurrentContext, Progress from fastmcp.server.context import Context # Create server with optional task support server = FastMCP("server_name") # Dependencies are properly typed and injected @server.tool(task=TaskConfig(mode="optional")) async def tool_name( param: str, ctx: Context = CurrentContext(), progress: Progress = Progress(), ) -> str: """Tool description.""" return "result" ``` **INCORRECT: Old MCP SDK imports** ```python # ❌ WRONG - These are from old MCP SDK, not FastMCP from mcp.server.fastmcp import Context, FastMCP from mcp.server.fastmcp import Progress # This will fail with modern FastMCP ``` --- ## 2. Common Pitfalls & Prevention ### 2.1 Pitfall: Using MCP SDK Testing with FastMCP **PROBLEM:** Attempting to use old MCP SDK testing utilities with FastMCP server ```python # ❌ WRONG - Won't work with FastMCP from mcp.testing import setup_test_server, Client from mcp_server_browser_use.server import serve def test_tool(): server = serve() # This won't work! setup_test_server is for old MCP SDK # FastMCP has different testing patterns ``` **SOLUTION:** Use FastMCP's Client directly ```python # ✅ CORRECT - FastMCP testing pattern from fastmcp import Client from mcp_server_browser_use.server import serve import pytest @pytest.fixture async def client(): app = serve() async with Client(app) as client: yield client @pytest.mark.anyio async def test_tool(client: Client): result = await client.call_tool("run_browser_agent", {"task": "Go to example.com"}) assert result.content is not None ``` ### 2.2 Pitfall: Confusing `Context()` with `CurrentContext()` **PROBLEM:** Using static Context instance instead of injection sentinel | Aspect | `Context()` | `CurrentContext()` | |--------|-------------|-------------------| | **Type** | Creates instance | Sentinel/marker | | **FastMCP behavior** | Static (not injected) | Recognized, triggers DI | | **Use case** | ❌ Don't use as default | ✅ Use as default | | **Runtime value** | May be None or stale | Injected at tool call time | ### 2.3 Pitfall: Not Checking Progress Availability **PROBLEM:** Assuming Progress is always available ```python # ❌ WRONG - May crash if Progress is None @server.tool() async def task(progress: Progress = Progress()) -> str: await progress.set_total(100) # Crashes if progress is None! await progress.increment() return "done" ``` **SOLUTION:** Always guard Progress operations ```python # ✅ CORRECT - Safe handling of optional Progress @server.tool() async def task(progress: Progress = Progress()) -> str: if progress: await progress.set_total(100) # Do work... if progress: await progress.increment() return "done" ``` ### 2.4 Pitfall: Forgetting TaskConfig Import **PROBLEM:** Using task=True instead of TaskConfig ```python # ❌ WRONG - task parameter is not boolean @server.tool(task=True) # What is this? Not recognized async def my_tool() -> str: return "result" ``` **SOLUTION:** Always import and use TaskConfig ```python # ✅ CORRECT from fastmcp import TaskConfig @server.tool(task=TaskConfig(mode="optional")) async def my_tool(progress: Progress = Progress()) -> str: return "result" ``` ### 2.5 Pitfall: Progress Operations Without Await **PROBLEM:** Forgetting async/await on Progress methods ```python # ❌ WRONG - Progress methods are async @server.tool() async def task(progress: Progress = Progress()) -> str: if progress: progress.set_message("Starting...") # Missing await! progress.set_total(100) # Missing await! return "done" ``` **SOLUTION:** Always await Progress operations ```python # ✅ CORRECT @server.tool() async def task(progress: Progress = Progress()) -> str: if progress: await progress.set_message("Starting...") await progress.set_total(100) return "done" ``` --- ## 3. Test Patterns for FastMCP Servers ### 3.1 Basic FastMCP Client Fixture ```python """Test fixture for FastMCP in-memory testing.""" import pytest from collections.abc import AsyncGenerator from fastmcp import Client from mcp_server_browser_use.server import serve @pytest.fixture def anyio_backend(): """Use asyncio for async tests.""" return "asyncio" @pytest.fixture async def client(monkeypatch) -> AsyncGenerator[Client, None]: """ Create an in-memory FastMCP client for testing. - Sets environment variables before importing server - Creates server instance - Yields client for tests - Cleans up automatically """ # Set environment variables BEFORE importing server (config reads them) monkeypatch.setenv("MCP_LLM_PROVIDER", "openai") monkeypatch.setenv("MCP_LLM_MODEL_NAME", "gpt-4") monkeypatch.setenv("OPENAI_API_KEY", "test-key-12345") monkeypatch.setenv("MCP_BROWSER_HEADLESS", "true") # Import server AFTER env vars are set from mcp_server_browser_use.server import serve app = serve() # Create client connection async with Client(app) as client: yield client ``` ### 3.2 Tool Testing Pattern ```python """Test MCP tools with mocking.""" import pytest from unittest.mock import AsyncMock, MagicMock, patch from fastmcp import Client class TestRunBrowserAgent: """Test the run_browser_agent tool.""" @pytest.mark.anyio async def test_run_browser_agent_success(self, client: Client): """Should successfully run browser agent with mocked dependencies.""" # Mock the agent mock_agent = MagicMock() mock_result = MagicMock() mock_result.final_result.return_value = "Task completed: Found 10 results" mock_agent.run = AsyncMock(return_value=mock_result) mock_llm = MagicMock() # Patch dependencies before calling tool with ( patch("mcp_server_browser_use.server.get_llm", return_value=mock_llm), patch("mcp_server_browser_use.server.Agent", return_value=mock_agent), ): # Call tool through FastMCP client result = await client.call_tool( "run_browser_agent", {"task": "Go to example.com"} ) # FastMCP returns CallToolResult with content list assert result.content is not None assert len(result.content) > 0 assert "Task completed" in result.content[0].text @pytest.mark.anyio async def test_run_browser_agent_with_optional_params(self, client: Client): """Should accept optional parameters.""" mock_agent = MagicMock() mock_result = MagicMock() mock_result.final_result.return_value = "Done" mock_agent.run = AsyncMock(return_value=mock_result) with ( patch("mcp_server_browser_use.server.get_llm", return_value=MagicMock()), patch("mcp_server_browser_use.server.Agent", return_value=mock_agent) as agent_class, ): # Call with optional max_steps parameter await client.call_tool( "run_browser_agent", {"task": "Test task", "max_steps": 5} ) # Verify Agent was instantiated with correct max_steps call_kwargs = agent_class.call_args[1] assert call_kwargs["max_steps"] == 5 @pytest.mark.anyio async def test_run_browser_agent_error_handling(self, client: Client): """Should handle initialization errors gracefully.""" from mcp_server_browser_use.exceptions import LLMProviderError with patch("mcp_server_browser_use.server.get_llm") as mock_get_llm: mock_get_llm.side_effect = LLMProviderError("API key missing") result = await client.call_tool( "run_browser_agent", {"task": "Test"} ) # Tool should return error message, not raise assert result.content is not None assert len(result.content) > 0 assert "Error" in result.content[0].text ``` ### 3.3 Tool List Verification ```python @pytest.mark.anyio async def test_list_all_tools(client: Client): """Verify all expected tools are registered.""" tools = await client.list_tools() tool_names = [tool.name for tool in tools] # Verify expected tools exist assert "run_browser_agent" in tool_names assert "run_deep_research" in tool_names # Verify tool schema agent_tool = next(t for t in tools if t.name == "run_browser_agent") assert agent_tool.description is not None assert "task" in str(agent_tool.inputSchema) ``` ### 3.4 Parametrized Testing ```python @pytest.mark.parametrize("max_steps,expected_calls", [ (None, 1), # Default max_steps used (5, 1), # Explicit max_steps used (10, 1), # Different explicit value ]) @pytest.mark.anyio async def test_agent_max_steps_variants(client: Client, max_steps, expected_calls): """Test various max_steps configurations.""" mock_agent = MagicMock() mock_agent.run = AsyncMock(return_value=MagicMock(final_result=lambda: "Done")) with patch("mcp_server_browser_use.server.Agent", return_value=mock_agent): params = {"task": "Test"} if max_steps is not None: params["max_steps"] = max_steps await client.call_tool("run_browser_agent", params) # Verify Agent was called with correct parameters assert mock_agent.run.call_count == expected_calls ``` ### 3.5 Mocking Patterns for Complex Dependencies ```python """Mocking patterns for browser-use specific components.""" @pytest.fixture def mock_browser_profile(): """Mock BrowserProfile for testing.""" profile = MagicMock() profile.headless = True return profile @pytest.fixture def mock_llm(): """Mock LLM for testing.""" llm = MagicMock() llm.ainvoke = AsyncMock(return_value=MagicMock(completion="Test response")) return llm @pytest.fixture def mock_agent(): """Mock Agent for testing.""" agent = MagicMock() result = MagicMock() result.final_result.return_value = "Agent completed" result.history = [] agent.run = AsyncMock(return_value=result) return agent @pytest.mark.anyio async def test_with_mocked_dependencies( client: Client, mock_agent, mock_llm, mock_browser_profile, ): """Test with all dependencies mocked.""" with ( patch("mcp_server_browser_use.server.Agent", return_value=mock_agent), patch("mcp_server_browser_use.server.get_llm", return_value=mock_llm), patch("mcp_server_browser_use.server.BrowserProfile", return_value=mock_browser_profile), ): result = await client.call_tool( "run_browser_agent", {"task": "Go to example.com"} ) assert result.content is not None ``` --- ## 4. Documentation Consultation Guide ### When to Check FastMCP Context7 Documentation Check [Context7 FastMCP Documentation](https://context7.com) when: 1. **Dependency Injection Issues** - "How do I inject request context?" - "What dependencies are available?" - Search: `CurrentContext`, `Progress`, `Context` 2. **Tool Configuration** - "How do I make a tool run in background?" - "What does task=True vs task=False mean?" - Search: `TaskConfig`, `task mode`, `background execution` 3. **Progress Reporting** - "How do I report progress to clients?" - "What methods are available on Progress?" - Search: `Progress`, `set_message`, `set_total`, `increment` 4. **Error Handling** - "What exceptions should tools raise?" - "How do I handle validation errors?" - Search: `exceptions`, `error handling`, `validation` 5. **Testing** - "How do I test FastMCP tools?" - "What's the FastMCP test client?" - Search: `testing`, `Client`, `test fixtures` ### Quick Reference URLs - **Dependencies:** Context7 > FastMCP > Execution > Dependencies - **Tasks:** Context7 > FastMCP > Execution > Tasks - **Progress:** Context7 > FastMCP > Execution > Progress - **Testing:** Context7 > FastMCP > Testing --- ## 5. Migration Checklist for Old Projects When migrating from old MCP SDK to FastMCP: - [ ] **Update imports** - Replace `from mcp.server.fastmcp import` with `from fastmcp import` - Add `from fastmcp.dependencies import CurrentContext, Progress` - [ ] **Fix Context injection** - Replace `ctx: Context = Context()` with `ctx: Context = CurrentContext()` - Verify all tools using context have this pattern - [ ] **Fix Progress handling** - Change `progress: Optional[Progress] = None` to `progress: Progress = Progress()` - Add guards: `if progress:` before operations - [ ] **Update tool decorators** - Add `task=TaskConfig(mode="optional")` for async operations - Verify decorator import: `from fastmcp import TaskConfig` - [ ] **Update tests** - Replace MCP SDK test utilities with `from fastmcp import Client` - Use `@pytest.mark.anyio` and `async with Client(app)` - Update mock patches to FastMCP module paths - [ ] **Update documentation** - Update API docs showing tool signatures - Document which tools support background execution - Update examples with new import statements --- ## 6. Key Takeaways | Issue | Prevention | |-------|-----------| | Static Context instances | Always use `CurrentContext()` as default | | Missing Progress checks | Always guard Progress ops with `if progress:` | | Broken test setup | Use `Client(app)` not MCP SDK test utilities | | Task config confusion | Import and use `TaskConfig(mode="...")` | | Async/await mistakes | Remember Progress methods are async | | Stale configuration | Set env vars BEFORE importing server | | Import confusion | Keep FastMCP imports consistent across codebase | --- ## Summary The FastMCP migration of mcp-browser-use revealed these core patterns: 1. **Dependency Injection Correctness**: Use `CurrentContext()` and `Progress()` as sentinels, not instances 2. **Defensive Progress Handling**: Always check if Progress exists before using 3. **FastMCP-Native Testing**: Replace old MCP SDK patterns with FastMCP's Client-based approach 4. **Task Configuration**: Use `TaskConfig` for background-capable tools 5. **Configuration Timing**: Load environment variables before importing server module Following these strategies prevents common FastMCP pitfalls and ensures reliable, maintainable MCP servers.

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/Saik0s/mcp-browser-use'

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