# Agent Evaluation Source: https://mcp-eval.ai/agent-evaluation Treat your agent as the system under test. Define scenarios, assert behavior, and measure efficiency and quality. Evaluate your agent’s reasoning, tool use, recovery, and quality by driving it through realistic tasks. ### Define the test agent * Global default: ```python import mcp_eval from mcp_agent.agents.agent_spec import AgentSpec mcp_eval.use_agent( AgentSpec(name="Fetcher", instruction="You fetch.", server_names=["fetch"]) # see [Settings](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/config.py) ) ``` * Per‑test override with `with_agent` (place above `@task`): ```python from mcp_eval.core import with_agent, task from mcp_agent.agents.agent import Agent @with_agent(Agent(name="Custom", instruction="Custom", server_names=["fetch"])) # see [Core](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/core.py) @task("Custom agent test") async def test_custom(agent, session): resp = await agent.generate_str("Fetch https://example.com") ``` * Factory for parallel safety: ```python from mcp_eval.config import use_agent_factory from mcp_agent.agents.agent import Agent def make_agent(): return Agent(name="Isolated", instruction="...", server_names=["fetch"]) # see [Settings](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/config.py) use_agent_factory(make_agent) ``` More patterns: [agent\_definition\_examples.py](https://github.com/lastmile-ai/mcp-eval/blob/main/examples/mcp_server_fetch/agent_definition_examples.py). ### What to measure * Tool behavior: `Expect.tools.was_called`, `called_with`, `sequence`, `output_matches` * Efficiency and iterations: `Expect.performance.max_iterations`, `Expect.path.efficiency` * Quality: `Expect.judge.llm`, `Expect.judge.multi_criteria` * Performance: response times, concurrency (see metrics) ```python # Efficiency and iteration bounds await session.assert_that(Expect.performance.max_iterations(3)) # Tool behavior and outputs await session.assert_that(Expect.tools.was_called("fetch")) await session.assert_that(Expect.tools.output_matches("fetch", {"isError": False}, match_type="partial")) # Path and sequence await session.assert_that(Expect.tools.sequence(["fetch"], allow_other_calls=True)) await session.assert_that(Expect.path.efficiency(expected_tool_sequence=["fetch"], allow_extra_steps=1)) ``` ### Styles for agent evals * Decorator tests: [test\_decorator\_style.py](https://github.com/lastmile-ai/mcp-eval/blob/main/examples/mcp_server_fetch/tests/test_decorator_style.py) * Pytest style: [test\_pytest\_style.py](https://github.com/lastmile-ai/mcp-eval/blob/main/examples/mcp_server_fetch/tests/test_pytest_style.py) * Datasets: [test\_dataset\_style.py](https://github.com/lastmile-ai/mcp-eval/blob/main/examples/mcp_server_fetch/tests/test_dataset_style.py) ### Inspecting spans and metrics ```python metrics = session.get_metrics() span_tree = session.get_span_tree() ``` Sources: * Session/agent: [session.py](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/session.py) * Catalog: [catalog.py](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/catalog.py) * Evaluators: [evaluators/](https://github.com/lastmile-ai/mcp-eval/tree/main/src/mcp_eval/evaluators/) {/* TODO: Add example screenshots of an agent’s failed vs passed run with metrics breakdown and tool coverage. */} # API: Catalog Source: https://mcp-eval.ai/api-catalog Master the Expect API for powerful assertions on content, tools, performance, and more. > The `Expect` API is your Swiss Army knife for MCP testing. Use it to assert everything from simple content checks to complex path efficiency and LLM quality judgments. ## Quick reference The `Expect` namespace provides intuitive access to all evaluator factories: ```python from mcp_eval.catalog import Expect # Content assertions Expect.content.contains("success") Expect.content.regex(r"\d+ results?") # Tool assertions Expect.tools.was_called("fetch") Expect.tools.success_rate(min_rate=0.95) Expect.tools.sequence(["validate", "process", "format"]) # Performance assertions Expect.performance.max_iterations(3) Expect.performance.response_time_under(5000) # LLM judge assertions Expect.judge.llm("Must be professional and accurate") Expect.judge.multi_criteria([criterion1, criterion2]) # Path efficiency assertions Expect.path.efficiency( expected_tool_sequence=["fetch", "parse"], allow_extra_steps=1 ) ``` ## Content namespace Validate the actual text content returned by your agent: ### Basic content checks ```python # Check if content contains a substring await session.assert_that( Expect.content.contains("Example Domain"), response=response, name="has_expected_text" ) # Exact match (use regex or precise substring) await session.assert_that( Expect.content.regex(r"^The answer is 42$"), response=response ) # Regular expression matching await session.assert_that( Expect.content.regex(r"Temperature: \d+°[CF]"), response=response, name="has_temperature" ) ``` ### Advanced content patterns ```python # Case-insensitive matching await session.assert_that( Expect.content.contains("SUCCESS", case_sensitive=False) ) # Multiple conditions (all must pass) for expected in ["result", "complete", "successful"]: await session.assert_that( Expect.content.contains(expected), response=response ) ``` ## Tools namespace Verify tool usage patterns and success rates: ### Basic tool checks ```python # Verify a specific tool was called await session.assert_that( Expect.tools.was_called("calculator"), name="used_calculator" ) # Check tool wasn't called (negative assertion) await session.assert_that( Expect.tools.count("dangerous_tool", 0), name="safety_check" ) ``` ### Tool sequences and patterns ```python # Verify exact sequence of tools await session.assert_that( Expect.tools.sequence(["auth", "fetch", "parse"]), name="correct_order" ) # Check tool was called with specific arguments await session.assert_that( Expect.tools.called_with( tool_name="fetch", expected_args={"url": "https://api.example.com"} ) ) # Verify success rate across all tool calls await session.assert_that( Expect.tools.success_rate(min_rate=0.95), name="high_reliability" ) ``` ### Tool output validation ```python # Check specific tool's output await session.assert_that( Expect.tools.output_matches( tool_name="weather_api", expected_output="sunny", match_type="contains" # or "exact", "regex", "partial" ) ) ``` ## Performance namespace Ensure your agent meets performance requirements: ### Response time and efficiency ```python # Maximum response time in milliseconds await session.assert_that( Expect.performance.response_time_under(5000), name="fast_response" ) # Limit conversation iterations await session.assert_that( Expect.performance.max_iterations(3), name="efficient_solution" ) ``` ### Resource usage ```python # Inspect tokens/cost programmatically via session.get_metrics() metrics = session.get_metrics() # LLM token usage (if available) if metrics.llm_metrics: print(f"Input tokens: {metrics.llm_metrics.input_tokens}") print(f"Output tokens: {metrics.llm_metrics.output_tokens}") print(f"Total tokens: {metrics.llm_metrics.total_tokens}") # Cost estimate (if computed) print(f"Estimated cost: ${metrics.cost_estimate:.4f}") ``` ## Judge namespace Use LLM judges for nuanced quality evaluation: ### Simple rubric evaluation ```python # Basic quality check await session.assert_that( Expect.judge.llm( rubric=""" The response should: - Be professional and courteous - Provide accurate information - Be concise (under 100 words) """, min_score=0.8, include_input=True # Give judge full context ), response=response, name="quality_check" ) ``` ### Multi-criteria evaluation ```python from mcp_eval.evaluators import EvaluationCriterion # Define weighted criteria criteria = [ EvaluationCriterion( name="accuracy", description="All facts are correct", weight=3.0, # Most important min_score=0.9 ), EvaluationCriterion( name="completeness", description="Addresses all user requirements", weight=2.0, min_score=0.8 ), EvaluationCriterion( name="clarity", description="Easy to understand", weight=1.0, min_score=0.7 ) ] # Apply multi-criteria judge await session.assert_that( Expect.judge.multi_criteria( criteria=criteria, aggregate_method="weighted", # or "min", "average" require_all_pass=False, use_cot=True # Chain-of-thought reasoning ), response=response, name="comprehensive_quality" ) ``` ### Custom judge configuration ```python # Use specific model for judging judge = Expect.judge.llm( rubric="Evaluate for technical accuracy", min_score=0.85, model="claude-3-opus-20240229" ) await session.assert_that(judge, response=response) ``` ## Path namespace Enforce optimal execution paths: ### Golden path enforcement ```python # Define the ideal execution path await session.assert_that( Expect.path.efficiency( expected_tool_sequence=["validate", "process", "save"], tool_usage_limits={ "validate": 1, # Should validate only once "process": 1, # Process only once "save": 1 # Save only once }, allow_extra_steps=0, penalize_backtracking=True ), name="golden_path" ) ``` ### Flexible path checking ```python # Allow some variation while ensuring key waypoints await session.assert_that( # For waypoint/pattern checks, combine Expect.tools.sequence and # Expect.tools.count along with Expect.path.efficiency ) ``` ## Combining assertions Create comprehensive test scenarios by combining multiple assertions: ```python @task("Complete workflow test") async def test_full_workflow(agent, session): """Test the entire user journey with multiple checkpoints.""" response = await agent.generate_str( "Fetch weather for NYC and format it nicely" ) # Content quality await session.assert_that( Expect.content.regex(r"\d+°[CF]"), response=response, name="has_temperature" ) # Tool usage await session.assert_that( Expect.tools.sequence(["weather_api", "format_tool"]), name="correct_tools" ) # Performance await session.assert_that( Expect.performance.response_time_under(3000), name="fast_enough" ) # Quality judgment await session.assert_that( Expect.judge.llm( "Output should be well-formatted and user-friendly", min_score=0.8 ), response=response, name="quality_output" ) ``` ## Best practices **Start simple, then add complexity:** Begin with basic content assertions, then layer on tool checks, performance requirements, and quality judgments as needed. **Avoid over-constraining:** Too many strict assertions can make tests brittle. Focus on what truly matters for your use case. **Use descriptive names:** Always provide a `name` parameter for your assertions. This makes debugging much easier when tests fail. ## See also Every available assertion with examples Build your own domain-specific evaluators # API: Config Source: https://mcp-eval.ai/api-config Complete guide to mcp-eval configuration: settings, agents, providers, and programmatic control. > Configuration is the foundation of flexible testing. mcp-eval provides multiple ways to configure your tests, from simple YAML files to sophisticated programmatic control. ## Configuration hierarchy mcp-eval uses a layered configuration system (highest priority first): 1. **Programmatic overrides** - Set in code 2. **Environment variables** - Set in shell or CI 3. **Config files** - `mcpeval.yaml` and `mcpeval.secrets.yaml` 4. **Defaults** - Built-in sensible defaults ## The MCPEvalSettings model The complete configuration structure: ```python from mcp_eval.config import MCPEvalSettings # Full settings structure settings = MCPEvalSettings( # Judge configuration judge={ "provider": "anthropic", "model": "claude-3-5-sonnet-20241022", "min_score": 0.8, "system_prompt": "You are a helpful test judge", "max_tokens": 2000, "temperature": 0.0 }, # Metrics collection metrics={ "collect_tool_calls": True, "collect_tokens": True, "collect_costs": True, "collect_timings": True, "include_thinking": False }, # Reporting configuration reporting={ "formats": ["json", "markdown", "html"], "output_dir": "test-reports", "include_traces": True, "include_conversation": True, "timestamp_format": "%Y%m%d_%H%M%S" }, # Execution control execution={ "max_concurrency": 5, "timeout_seconds": 300, "max_retries": 3, "retry_delay_seconds": 5, "fail_fast": False, "verbose": True }, # Default provider settings provider="anthropic", model="claude-3-5-sonnet-20241022", # Default servers default_servers=["fetch", "filesystem"], # Default agent default_agent="default" ) ``` ## Loading configuration ### Automatic discovery ```python from mcp_eval.config import load_config # Discovers config files from current directory upward settings = load_config() # Or specify a path settings = load_config("/path/to/project") # Or pass a dict settings = load_config({ "provider": "openai", "model": "gpt-4-turbo-preview" }) ``` ### Manual loading ```python from mcp_eval.config import MCPEvalSettings import yaml # Load from YAML file with open("custom_config.yaml") as f: config_dict = yaml.safe_load(f) settings = MCPEvalSettings(**config_dict) # Load and merge multiple sources base_config = yaml.safe_load(open("base.yaml")) secrets = yaml.safe_load(open("secrets.yaml")) overrides = {"execution": {"verbose": True}} # Merge configurations full_config = {**base_config, **secrets, **overrides} settings = MCPEvalSettings(**full_config) ``` ## Updating configuration ### Global updates ```python from mcp_eval.config import update_config, get_settings # Update specific fields update_config({ "execution": { "max_concurrency": 10, "timeout_seconds": 600 }, "reporting": { "output_dir": "custom-reports" } }) # Get current settings current = get_settings() print(f"Timeout: {current.execution.timeout_seconds}s") ``` ### Scoped configuration ```python from mcp_eval.config import use_config import contextlib # Temporarily use different config with use_config(custom_settings): # Tests here use custom_settings await run_tests() # Original config restored # Or use context manager @contextlib.contextmanager def production_config(): original = get_settings() try: update_config({ "provider": "anthropic", "model": "claude-3-opus-20240229", "execution": {"max_retries": 5} }) yield finally: use_config(original) with production_config(): await run_critical_tests() ``` ## Agent configuration ### Using named agents ```python from mcp_eval.config import use_agent # Use agent defined in mcpeval.yaml use_agent("specialized_agent") # Agents are defined in config like: # agents: # specialized_agent: # model: claude-3-opus-20240229 # provider: anthropic # instruction: "You are a specialized test agent" # server_names: ["custom_server"] ``` ### Agent factory pattern ```python from mcp_eval.config import use_agent_factory from mcp_eval.agent import Agent def create_dynamic_agent(): """Create agent based on runtime conditions.""" if os.getenv("TEST_ENV") == "production": return Agent( model="claude-3-opus-20240229", instruction="Be extremely thorough" ) else: return Agent( model="claude-3-5-sonnet-20241022", instruction="Standard testing" ) # Register the factory use_agent_factory(create_dynamic_agent) ``` ### Direct agent objects ```python from mcp_eval.config import use_agent_object from mcp_eval.agent import Agent # Create and configure agent my_agent = Agent( model="claude-3-5-sonnet-20241022", provider="anthropic", instruction="""You are a security-focused test agent. Always check for vulnerabilities and edge cases.""", server_names=["security_scanner", "filesystem"], temperature=0.0, # Deterministic max_tokens=4000 ) # Use this specific agent use_agent_object(my_agent) ``` ### Agent configuration in tests ```python from mcp_eval.core import task, with_agent from mcp_eval.agent import AgentConfig # Use different agents for different tests @with_agent("fast_agent") @task("Quick test") async def test_fast(agent): # Uses fast_agent configuration pass @with_agent(AgentConfig( model="claude-3-opus-20240229", instruction="Be extremely thorough", max_iterations=10 )) @task("Thorough test") async def test_thorough(agent): # Uses inline configuration pass ``` ## Programmatic defaults Set global defaults programmatically: ```python from mcp_eval.config import ProgrammaticDefaults # Set default agent for all tests ProgrammaticDefaults.set_default_agent(my_agent) # Set default servers ProgrammaticDefaults.set_default_servers(["fetch", "calculator"]) # Set default provider configuration ProgrammaticDefaults.set_provider_config({ "provider": "openai", "model": "gpt-4-turbo-preview", "api_key": os.getenv("OPENAI_API_KEY") }) # Clear all programmatic defaults ProgrammaticDefaults.clear() ``` ## Environment variables ### Provider configuration ```bash # API keys export ANTHROPIC_API_KEY="sk-ant-..." export OPENAI_API_KEY="sk-..." export GOOGLE_API_KEY="..." # Provider selection export MCPEVAL_PROVIDER="anthropic" export MCPEVAL_MODEL="claude-3-5-sonnet-20241022" # Provider-specific settings export ANTHROPIC_BASE_URL="https://api.anthropic.com" export OPENAI_ORG_ID="org-..." ``` ### Execution control ```bash # Timeouts and retries export MCPEVAL_TIMEOUT_SECONDS="600" export MCPEVAL_MAX_RETRIES="5" export MCPEVAL_RETRY_DELAY="10" # Concurrency export MCPEVAL_MAX_CONCURRENCY="10" # Verbosity export MCPEVAL_VERBOSE="true" export MCPEVAL_DEBUG="true" ``` ### Reporting ```bash # Output configuration export MCPEVAL_OUTPUT_DIR="/tmp/test-reports" export MCPEVAL_REPORT_FORMATS="json,html,markdown" export MCPEVAL_INCLUDE_TRACES="true" ``` ## Configuration validation ### Validate on load ```python from mcp_eval.config import load_config, validate_config try: settings = load_config() validate_config(settings) except ValueError as e: print(f"Invalid configuration: {e}") # Handle invalid config ``` ### Custom validation ```python def validate_custom_settings(settings: MCPEvalSettings): """Add custom validation rules.""" # Ensure API key is set if settings.provider == "anthropic": if not os.getenv("ANTHROPIC_API_KEY"): raise ValueError("Anthropic API key required") # Validate model compatibility if settings.judge.provider == "openai": valid_models = ["gpt-4", "gpt-4-turbo-preview"] if settings.judge.model not in valid_models: raise ValueError(f"Judge model must be one of {valid_models}") # Ensure timeout is reasonable if settings.execution.timeout_seconds > 3600: raise ValueError("Timeout cannot exceed 1 hour") return True # Use in your test setup settings = load_config() if not validate_custom_settings(settings): sys.exit(1) ``` ## Advanced patterns ### Dynamic configuration based on environment ```python import os from mcp_eval.config import load_config, update_config def configure_for_environment(): """Adjust config based on environment.""" base_config = load_config() env = os.getenv("TEST_ENV", "development") if env == "production": update_config({ "provider": "anthropic", "model": "claude-3-opus-20240229", "execution": { "max_retries": 5, "timeout_seconds": 600, "fail_fast": True }, "judge": { "min_score": 0.9 # Stricter in production } }) elif env == "ci": update_config({ "execution": { "max_concurrency": 2, # Limited resources in CI "verbose": True }, "reporting": { "formats": ["json"], # Machine-readable only "output_dir": "/tmp/ci-reports" } }) else: # development update_config({ "execution": { "verbose": True, "max_retries": 1 }, "reporting": { "formats": ["html"], # Interactive reports } }) configure_for_environment() ``` ### Configuration inheritance ```python class BaseTestConfig: """Base configuration for all tests.""" BASE_SETTINGS = { "provider": "anthropic", "model": "claude-3-5-sonnet-20241022", "execution": { "timeout_seconds": 300, "max_retries": 3 } } class IntegrationTestConfig(BaseTestConfig): """Config for integration tests.""" SETTINGS = { **BaseTestConfig.BASE_SETTINGS, "execution": { **BaseTestConfig.BASE_SETTINGS["execution"], "timeout_seconds": 600, # Longer timeout }, "default_servers": ["fetch", "database", "cache"] } class UnitTestConfig(BaseTestConfig): """Config for unit tests.""" SETTINGS = { **BaseTestConfig.BASE_SETTINGS, "execution": { **BaseTestConfig.BASE_SETTINGS["execution"], "timeout_seconds": 60, # Quick tests }, "default_servers": ["mock_server"] } # Use in tests from mcp_eval.config import use_config if test_type == "integration": use_config(IntegrationTestConfig.SETTINGS) else: use_config(UnitTestConfig.SETTINGS) ``` ### Config hot-reloading ```python import watchdog.observers import watchdog.events class ConfigReloader(watchdog.events.FileSystemEventHandler): """Reload config when files change.""" def on_modified(self, event): if event.src_path.endswith("mcpeval.yaml"): print("Config changed, reloading...") try: new_config = load_config() use_config(new_config) print("✅ Config reloaded successfully") except Exception as e: print(f"❌ Failed to reload: {e}") # Watch for changes observer = watchdog.observers.Observer() observer.schedule(ConfigReloader(), ".", recursive=False) observer.start() ``` ## Best practices **Separate secrets:** Always keep API keys and sensitive data in `mcpeval.secrets.yaml` or environment variables, never in your main config file. **Validate early:** Validate your configuration at the start of your test runs to catch issues before tests begin executing. **Use environment-specific configs:** Different environments (dev, staging, prod) should have different configuration profiles for appropriate testing rigor. ## Debugging configuration ```python from mcp_eval.config import get_settings, print_config # Print current configuration print_config() # Or get as dict for inspection settings = get_settings() config_dict = settings.model_dump() import json print(json.dumps(config_dict, indent=2)) # Check specific values print(f"Provider: {settings.provider}") print(f"Model: {settings.model}") print(f"Timeout: {settings.execution.timeout_seconds}s") print(f"Output dir: {settings.reporting.output_dir}") ``` ## See also Complete configuration reference Configuring test agents Initial setup and configuration Configuration best practices # API: Core Source: https://mcp-eval.ai/api-core Essential decorators and primitives for writing mcp-eval tests: @task, @with_agent, @parametrize, and more. > The Core API provides the fundamental building blocks for writing mcp-eval tests. These decorators and utilities make your tests clean, reusable, and powerful. ## Quick reference ```python from mcp_eval.core import ( task, # Define a test task with_agent, # Specify which agent to use parametrize, # Run tests with multiple inputs setup, # Run before tests teardown, # Run after tests TestResult # Test execution results ) ``` ## The @task decorator The foundation of every mcp-eval test: ### Basic usage ```python from mcp_eval.core import task from mcp_eval.catalog import Expect @task("My first test") async def test_basic(agent, session): """A simple test that checks basic functionality.""" response = await agent.generate_str("Hello, world!") await session.assert_that( Expect.content.contains("Hello"), response=response ) ``` ### Task parameters ```python # Compose multiple assertions and give them names for clear reporting @task("Fetch and summarize") async def test_fetch_and_summarize(agent, session): response = await agent.generate_str( "Fetch https://example.com and summarize in one sentence" ) await session.assert_that( Expect.tools.was_called("fetch"), name="fetch_called" ) await session.assert_that( Expect.content.contains("Example Domain"), response=response, name="has_expected_text" ) await session.assert_that( Expect.performance.max_iterations(3), name="efficient" ) ``` ### Task with custom configuration ```python @task("Expensive test") async def test_with_config(agent, session): response = await agent.generate_str("Complex analysis task") # Test continues... ``` ## The @with\_agent decorator Specify which agent configuration to use: ### Using named agents ```python @with_agent("default") @task("Test with default agent") async def test_default(agent): # Uses the 'default' agent from config response = await agent.generate_str("Test prompt") ``` ### Multiple agent configurations ```python # Define agents in mcpeval.yaml first @with_agent("specialized_agent") @task("Test specialized behavior") async def test_specialized(agent): # Uses a different agent configuration pass @with_agent("minimal_agent") @task("Test minimal setup") async def test_minimal(agent): # Uses yet another configuration pass ``` ### Inline agent configuration ```python from mcp_agent.agents.agent_spec import AgentSpec @with_agent(AgentSpec( name="code_reviewer", instruction="You are a code reviewer. Be thorough and critical.", server_names=["filesystem", "git"] )) @task("Code review test") async def test_code_review(agent): response = await agent.generate_str("Review this code: ...") ``` **Decorator order matters!** Always apply `@with_agent` above `@task` to ensure the agent is properly configured when the task runs. ```python # ✅ Correct @with_agent("default") @task("test") async def test_func(agent): ... # ❌ Wrong - will error @task("test") @with_agent("default") async def test_func(agent): ... ``` ## The @parametrize decorator Run the same test with different inputs: ### Basic parametrization ```python @with_agent("default") @parametrize("number", [1, 2, 5, 10, 100]) @task("Test with different numbers") async def test_numbers(agent, number): response = await agent.generate_str(f"Is {number} prime?") # Each number creates a separate test case await agent.assert_that( Expect.content.regex(r"(yes|no|prime|composite)") ) ``` ### Multiple parameters ```python @with_agent("default") @parametrize("operation", ["add", "subtract", "multiply"]) @parametrize("x", [1, 10, 100]) @parametrize("y", [2, 5]) @task("Test calculator operations") async def test_calculator(agent, operation, x, y): # Creates 3 * 3 * 2 = 18 test cases! prompt = f"Use the calculator to {operation} {x} and {y}" response = await agent.generate_str(prompt) # Verify the right tool was used await agent.assert_that( Expect.tools.was_called(f"calculator_{operation}") ) ``` ### Named scenarios Use `@parametrize("name,url,expected", [...])` to model named cases. ```python @with_agent("default") @parametrize( "name,url,expected", [ ("home", "https://example.com", "Example Domain"), ("httpbin_json", "https://httpbin.org/json", "slideshow"), ], ) @task("Fetch {name}") async def test_fetch_case(agent, session, name: str, url: str, expected: str): response = await agent.generate_str(f"Fetch {url}") await session.assert_that( Expect.tools.was_called("fetch"), name=f"{name}_fetch_called", ) await session.assert_that( Expect.content.contains(expected, case_sensitive=False), response=response, name=f"{name}_has_expected", ) ``` ### Dynamic parametrization ```python def get_test_cases(): """Generate test cases dynamically.""" import json with open("test_data.json") as f: return json.load(f)["test_cases"] @with_agent("default") @parametrize("test_case", get_test_cases()) @task("Dynamic test cases") async def test_dynamic(agent, test_case): response = await agent.generate_str(test_case["prompt"]) await agent.assert_that( Expect.content.contains(test_case["expected"]), response=response ) ``` ## Setup and teardown Run code before and after your tests: ### Simple setup/teardown ```python from mcp_eval.core import setup, teardown import os import tempfile test_dir = None @setup def prepare_test_environment(): """Create temporary test directory.""" global test_dir test_dir = tempfile.mkdtemp(prefix="mcp_test_") print(f"🚀 Created test directory: {test_dir}") # Set up test files with open(f"{test_dir}/test.txt", "w") as f: f.write("Test content") @teardown def cleanup_test_environment(): """Clean up after tests.""" global test_dir if test_dir and os.path.exists(test_dir): import shutil shutil.rmtree(test_dir) print(f"🧹 Cleaned up {test_dir}") ``` ### Async setup/teardown ```python @setup async def async_prepare(): """Setup that requires async operations.""" # Connect to database await db.connect() # Seed test data await db.execute("INSERT INTO test_table ...") print("✅ Database ready") @teardown async def async_cleanup(): """Async cleanup operations.""" await db.execute("DELETE FROM test_table WHERE ...") await db.disconnect() ``` ### Setup with validation ```python @setup def validate_environment(): """Ensure test environment is properly configured.""" import sys # Check Python version if sys.version_info < (3, 10): raise RuntimeError("Tests require Python 3.10+") # Check required environment variables required_vars = ["ANTHROPIC_API_KEY", "TEST_SERVER_URL"] missing = [var for var in required_vars if not os.getenv(var)] if missing: raise RuntimeError(f"Missing environment variables: {missing}") # Check MCP servers are accessible from mcp_eval.utils import check_server_health if not check_server_health("my_server"): raise RuntimeError("MCP server 'my_server' is not responding") print("✅ Environment validated successfully") ``` ## TestResult object Understanding test execution results: ### TestResult structure ```python from mcp_eval.core import TestResult # After a test runs, you get a TestResult: result = TestResult( id="test_123_abc", name="Test basic fetch", passed=True, duration_ms=1234.56, parameters={"url": "https://example.com"}, metrics={ "tool_calls": 2, "tokens_used": 500, "cost_usd": 0.01 }, evaluations=[ {"name": "content_check", "passed": True, "score": 1.0}, {"name": "performance", "passed": True, "details": "Under 2s"} ], error=None # Or error message if failed ) ``` ### Accessing TestResult in hooks ```python # Use CLI combined reports or session.get_metrics()/get_results() for summaries ``` ### Aggregating results ```python def analyze_test_results(results: list[TestResult]): """Analyze a batch of test results.""" total = len(results) passed = sum(1 for r in results if r.passed) total_duration = sum(r.duration_ms for r in results) total_cost = sum(r.metrics.get('cost_usd', 0) for r in results) print(f"\n📊 Test Summary:") print(f" Total tests: {total}") print(f" Passed: {passed}/{total} ({passed/total*100:.1f}%)") print(f" Total duration: {total_duration/1000:.2f}s") print(f" Total cost: ${total_cost:.4f}") # Find slowest tests slowest = sorted(results, key=lambda r: r.duration_ms, reverse=True)[:3] print(f"\n🐢 Slowest tests:") for result in slowest: print(f" {result.name}: {result.duration_ms:.0f}ms") # Find failed tests failed = [r for r in results if not r.passed] if failed: print(f"\n❌ Failed tests:") for result in failed: print(f" {result.name}: {result.error}") ``` ## Advanced patterns ### Conditional test execution Prefer selecting tests with your runner and environment rather than custom decorators. ```bash # Run a single test function (pytest-style selector supported by the runner for decorator tests) mcp-eval run tests/test_fetch.py::test_fetch_case # Run pytest tests (use pytest) uv run pytest -q tests ``` Using pytest marks for conditions (when running under pytest): ```python import os, pytest from mcp_eval.core import task @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skip on CI") @task("Local-only behavior") async def test_local_only(agent, session): response = await agent.generate_str("Do something local") # assertions... @pytest.mark.slow @task("Slow end-to-end scenario") async def test_slow_scenario(agent, session): # long-running flow... ... ``` Then select by mark: ```bash pytest -m "not slow" tests/ ``` ### Test dependencies Prefer independent tests; if ordering is required, orchestrate via your runner. ### Custom test context ```python from contextvars import ContextVar test_context = ContextVar('test_context', default={}) @task("Test with context") async def test_with_context(agent, session): # Set context for this test ctx = test_context.get().copy() ctx['test_id'] = session.test_id ctx['start_time'] = time.time() test_context.set(ctx) response = await agent.generate_str("Test prompt") # Context is available throughout the test duration = time.time() - ctx['start_time'] print(f"Test {ctx['test_id']} took {duration:.2f}s") ``` ## Best practices **Name your tests clearly:** Use descriptive names that explain what the test validates. This helps when reviewing test reports. **Avoid test interdependence:** Each test should be independent and not rely on side effects from other tests, unless explicitly using `depends_on`. **Use parametrize wisely:** While parametrization is powerful, too many parameter combinations can make tests slow. Consider grouping related parameters. ## Common patterns ### Testing error handling ```python @with_agent("default") @task("Test error recovery") async def test_error_handling(agent): # Trigger an error condition response = await agent.generate_str("Divide 10 by 0") # Verify graceful handling await agent.assert_that( Expect.content.regex(r"(error|cannot|undefined|infinity)"), name="handles_division_by_zero" ) # Verify no tool crashes # Check tool success via success_rate, e.g., Expect.tools.success_rate(1.0) ``` ### Testing multi-step workflows ```python @with_agent("default") @task("Test complete workflow") async def test_workflow(agent, session): # Step 1: Authentication auth_response = await agent.generate_str("Authenticate as test_user") await session.assert_that( Expect.tools.was_called("auth"), name="authentication_attempted" ) # Step 2: Fetch data data_response = await agent.generate_str("Get my profile data") await session.assert_that( Expect.tools.was_called("fetch_profile"), name="profile_fetched" ) # Step 3: Process process_response = await agent.generate_str("Summarize my activity") await session.assert_that( Expect.content.contains("summary"), response=process_response, name="summary_generated" ) # Verify the complete sequence await session.assert_that( Expect.tools.sequence(["auth", "fetch_profile", "summarize"]), name="correct_workflow_order" ) ``` ## See also Available assertions for your tests Managing test sessions and agents Writing maintainable tests Complete test examples # API: Session Source: https://mcp-eval.ai/api-session Master TestSession and TestAgent for orchestrating tests, assertions, and metrics collection. > The Session API is the heart of mcp-eval testing. It manages your agent's lifecycle, collects metrics, runs assertions, and produces comprehensive test results. ## Quick start The simplest way to create a test session: ```python from mcp_eval.session import test_session from mcp_eval.catalog import Expect async with test_session("my-test") as agent: # Agent is ready with MCP servers connected response = await agent.generate_str("Fetch https://example.com") # Run assertions await agent.assert_that( Expect.content.contains("Example Domain"), response=response ) ``` ## Core concepts ### TestSession The orchestrator that manages everything: * **Lifecycle management**: Starts/stops agents and MCP servers * **Tool discovery**: Automatically finds and registers MCP tools * **Metrics collection**: Tracks all interactions via OTEL * **Assertion execution**: Runs evaluators at the right time * **Report generation**: Produces test artifacts ### TestAgent A thin, friendly wrapper around your LLM agent: * **Simple interface**: Just `generate()` and `assert_that()` * **Automatic tracking**: All interactions are recorded * **Context preservation**: Maintains conversation state ## Creating sessions ### Basic session creation ```python # Using context manager (recommended) async with test_session("test-name") as agent: # Your test code here pass # Manual lifecycle (advanced) session = TestSession(test_name="test-name") agent = await session.__aenter__() try: # Your test code ... finally: await session.__aexit__(None, None, None) session.cleanup() ``` ### Session with custom configuration ```python from mcp_eval.session import test_session from mcp_agent.agents.agent_spec import AgentSpec spec = AgentSpec( name="custom", instruction="You are a helpful test assistant", server_names=["my_server"], ) async with test_session("custom-test", agent=spec) as agent: # Your test code pass ``` ## Agent interactions ### Generating responses ```python # Simple string generation response = await agent.generate_str("What is 2+2?") print(response) # "The answer is 4" # Full response object may be available depending on provider; prefer generate_str for portability ``` ### Multi-turn conversations ```python # Sessions maintain context response1 = await agent.generate_str("My name is Alice") response2 = await agent.generate_str("What's my name?") # response2 will correctly identify "Alice" ``` ## Assertions in depth ### Immediate vs. deferred assertions ```python # Immediate: evaluated right away (content, judge) await session.assert_that( Expect.content.contains("success"), response=response, # Required for immediate name="has_success" ) # Deferred: evaluated at session end (tools, performance, path) await session.assert_that( Expect.tools.was_called("calculator"), name="used_calculator" # No response needed ) # Force deferred evaluation at end await session.assert_that( Expect.content.contains("final"), response=response, when="end" # Defer even content checks ) ``` ### Assertion timing control ```python # Evaluate specific assertions immediately result = await session.evaluate_now_async( Expect.performance.response_time_under(5000), response=response, name="quick_response" ) if not result.passed: print(f"Too slow: {result.details}") # Take corrective action # Batch evaluate multiple assertions results = await session.evaluate_now_async( Expect.tools.success_rate(0.95), Expect.performance.max_iterations(3) ) ``` ### Named assertions for better reporting ```python # Always name your assertions for clarity await session.assert_that( Expect.content.regex(r"\d+ items? found"), response=response, name="item_count_format" # Appears in reports ) ``` ## Metrics and results ### Accessing metrics during tests ```python # Get current metrics metrics = session.get_metrics() print(f"Tool calls: {len(metrics.tool_calls)}") print(f"Total tokens: {metrics.total_tokens}") print(f"Duration so far: {metrics.total_duration_ms}ms") print(f"Estimated cost: ${metrics.total_cost_usd:.4f}") # Detailed tool information for call in metrics.tool_calls: print(f"Tool: {call.name}") print(f"Duration: {call.duration_ms}ms") print(f"Success: {call.success}") if not call.success: print(f"Error: {call.error}") ``` ### Getting test results ```python # Check if all assertions passed if session.all_passed(): print("✅ All tests passed!") else: print("❌ Some tests failed") # Get detailed results results = session.get_results() for result in results: print(f"Assertion: {result.name}") print(f"Passed: {result.passed}") if not result.passed: print(f"Reason: {result.details}") # Get pass/fail summary summary = session.get_summary() print(f"Passed: {summary['passed']}/{summary['total']}") print(f"Pass rate: {summary['pass_rate']:.1%}") ``` ### Duration tracking ```python # Get test duration duration_ms = session.get_duration_ms() print(f"Test took {duration_ms/1000:.2f} seconds") # Track specific operations from time import time start = time() response = await agent.generate_str("Complex task") operation_time = (time() - start) * 1000 if operation_time > 5000: print(f"Warning: Operation took {operation_time:.0f}ms") ``` ## OpenTelemetry traces ### Accessing trace data ```python # Get structured span tree span_tree = session.get_span_tree() def print_spans(span, indent=0): prefix = " " * indent print(f"{prefix}{span.name}: {span.duration_ms}ms") for child in span.children: print_spans(child, indent + 1) print_spans(span_tree) # Ensure traces are written to disk await session._ensure_traces_flushed() ``` ### Custom span attributes ```python # Add custom attributes to current span from opentelemetry import trace tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("custom_operation") as span: span.set_attribute("user_id", "123") span.set_attribute("operation_type", "validation") response = await agent.generate_str("Validate user input") ``` ## Artifacts and reporting ### Session artifacts ```python # Sessions automatically save artifacts session = await TestSession.create( test_name="my-test", output_dir="test-reports", # Custom output location save_artifacts=True # Enable artifact saving ) # After test completion, find artifacts at: # test-reports/my-test_[timestamp]/ # ├── trace.jsonl # OTEL traces # ├── results.json # Test results # ├── metrics.json # Performance metrics # └── conversation.json # Full conversation log ``` ### Programmatic report generation ```python # Generate reports programmatically from mcp_eval.reports import ReportGenerator generator = ReportGenerator(session) # Generate different formats await generator.save_json("results.json") await generator.save_markdown("results.md") await generator.save_html("results.html") # Get report data for custom processing report_data = generator.get_report_data() print(f"Test: {report_data['test_name']}") print(f"Duration: {report_data['duration_ms']}ms") print(f"Passed: {report_data['passed']}/{report_data['total']}") ``` ## Advanced patterns ### Custom session hooks ```python class CustomSession(TestSession): async def on_tool_call(self, tool_name: str, args: dict): """Hook called before each tool execution.""" print(f"About to call {tool_name} with {args}") # Validate tool usage if tool_name == "dangerous_tool": raise ValueError("Dangerous tool not allowed in tests") async def on_assertion_complete(self, result): """Hook called after each assertion.""" if not result.passed: # Log failures to external system await self.log_to_monitoring(result) ``` ### Session state management ```python # Store custom state in session session.state["test_user_id"] = "user_123" session.state["test_context"] = {"environment": "staging"} # Access state in assertions or hooks user_id = session.state.get("test_user_id") ``` ### Parallel session execution ```python import asyncio async def run_test(test_name: str, prompt: str): async with test_session(test_name) as agent: response = await agent.generate_str(prompt) await agent.assert_that( Expect.content.contains("success"), response=response ) return agent.session.all_passed() # Run multiple tests in parallel results = await asyncio.gather( run_test("test1", "Task 1"), run_test("test2", "Task 2"), run_test("test3", "Task 3") ) print(f"All passed: {all(results)}") ``` ## Best practices **Use context managers:** Always use `async with test_session()` to ensure proper cleanup, even if tests fail. **Name your assertions:** Always provide descriptive names for assertions. This makes debugging much easier when reviewing test reports. **Monitor metrics:** Check metrics during long-running tests to catch performance issues early. ## Error handling ```python try: async with test_session("error-test") as agent: response = await agent.generate_str("Test prompt") await agent.assert_that( Expect.content.contains("expected"), response=response ) except TimeoutError: print("Test timed out - increase timeout_seconds") except AssertionError as e: print(f"Assertion failed: {e}") except Exception as e: print(f"Unexpected error: {e}") # Session cleanup is still guaranteed ``` ## See also All available assertions Session configuration options Understanding test reports Deep dive into metrics # Assertions Source: https://mcp-eval.ai/assertions Unified assertion API with the Expect catalog: content, tools, performance, judges, path. > Assertions use a single entrypoint and an expressive catalog. Prefer structural checks for stability, and combine them with judges only when necessary. Use one entrypoint: ```python await session.assert_that(Expect.content.contains("Example"), response=resp) ``` Catalog source: [catalog.py](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/catalog.py) ## Immediate vs deferred * Immediate: content and judge (given `response`) * Deferred (requires final metrics): tools, performance, path ## Content * `Expect.content.contains(text, case_sensitive=False)` * `Expect.content.not_contains(text, case_sensitive=False)` * `Expect.content.regex(pattern, case_sensitive=False)` Examples: ```python resp = await agent.generate_str("Fetch https://example.com") await session.assert_that(Expect.content.contains("Example Domain"), response=resp) await session.assert_that(Expect.content.not_contains("Stack trace"), response=resp) await session.assert_that(Expect.content.regex(r"Example\\s+Domain"), response=resp) ``` ## Tools * `Expect.tools.was_called(name, min_times=1)` * `Expect.tools.called_with(name, {args})` * `Expect.tools.count(name, expected_count)` * `Expect.tools.success_rate(min_rate, tool_name=None)` * `Expect.tools.failed(name)` * `Expect.tools.output_matches(name, expected, field_path?, match_type?, case_sensitive?, call_index?)` * `Expect.tools.sequence([names], allow_other_calls=False)` Notes: * `field_path` supports nested dict/list access, e.g. `content[0].text` or `content.0.text` Examples: ```python # Verify a fetch occurred with expected output pattern in first content block await session.assert_that( Expect.tools.output_matches( tool_name="fetch", expected_output=r"use.*examples", match_type="regex", case_sensitive=False, field_path="content[0].text", ), name="fetch_output_match", ) # Verify sequence and counts await session.assert_that(Expect.tools.sequence(["fetch"], allow_other_calls=True)) await session.assert_that(Expect.tools.count("fetch", 1)) await session.assert_that(Expect.tools.success_rate(1.0, tool_name="fetch")) ``` ## Performance * `Expect.performance.max_iterations(n)` * `Expect.performance.response_time_under(ms)` Example: ```python await session.assert_that(Expect.performance.max_iterations(3)) await session.assert_that(Expect.performance.response_time_under(10_000)) ``` ## Judge * `Expect.judge.llm(rubric, min_score=0.8, include_input=False, require_reasoning=True)` * `Expect.judge.multi_criteria(criteria, aggregate_method="weighted", require_all_pass=False, include_confidence=True, use_cot=True, model=None)` Examples: ```python # Single-criterion rubric judge = Expect.judge.llm( rubric="Response should identify JSON format and summarize main points", min_score=0.8, include_input=True, ) await session.assert_that(judge, response=resp, name="quality_check") # Multi-criteria from mcp_eval.evaluators import EvaluationCriterion criteria = [ EvaluationCriterion(name="accuracy", description="Factual correctness", weight=2.0, min_score=0.8), EvaluationCriterion(name="completeness", description="Covers key points", weight=1.5, min_score=0.7), ] judge_mc = Expect.judge.multi_criteria(criteria, aggregate_method="weighted", use_cot=True) await session.assert_that(judge_mc, response=resp, name="multi_criteria") ``` ## Path * `Expect.path.efficiency(optimal_steps?, expected_tool_sequence?, allow_extra_steps=0, penalize_backtracking=True, penalize_repeated_tools=True, tool_usage_limits?, default_tool_limit=1)` Evaluators source: [evaluators/](https://github.com/lastmile-ai/mcp-eval/tree/main/src/mcp_eval/evaluators/) ### Examples ```python # Enforce exact order and single use of tools await session.assert_that( Expect.path.efficiency( expected_tool_sequence=["validate", "process", "save"], tool_usage_limits={"validate": 1, "process": 1, "save": 1}, allow_extra_steps=0, penalize_backtracking=True, ), name="golden_path", ) # Allow a retry of a fragile step without failing the whole path await session.assert_that( Expect.path.efficiency( expected_tool_sequence=["fetch", "parse"], tool_usage_limits={"fetch": 2, "parse": 1}, allow_extra_steps=1, penalize_repeated_tools=False, ), name="robust_path", ) ``` ### Tips * Prefer structural checks (`output_matches`) for tool outputs when possible for stability * Use `name` in `assert_that(..., name="...")` to label checks in reports * Combine judge + structural checks for high confidence Combine a minimal judge (e.g., rubric with `min_score=0.8`) with one or two structural checks (like `output_matches`) for resilient tests. Deferred assertions are evaluated when the session ends or when `when="end"` is used. If an assertion depends on final metrics (e.g., success rate), defer it. # Best Practices Source: https://mcp-eval.ai/best-practices Learn proven patterns and anti-patterns for testing MCP servers and agents. Write maintainable, reliable, and efficient tests that scale with your project. > 🌟 **Test like a pro!** These best practices come from real-world experience testing MCP servers and agents at scale. Follow these guidelines to build a robust, maintainable test suite. ## Quick best practice finder Jump to what you need: Writing effective tests Structuring test suites Choosing the right checks Fast and efficient testing Reducing flakiness Keeping tests healthy ## Test design principles ### 1. Test one thing at a time (by default) **✅ Do:** Focus each test on a single behavior or feature ```python @task("Test calculator addition") async def test_addition(agent, session): """Test ONLY addition functionality.""" response = await agent.generate_str("Calculate 5 + 3") await session.assert_that( Expect.tools.was_called("calculator"), Expect.content.contains("8"), response=response ) @task("Test calculator error handling") async def test_division_by_zero(agent, session): """Test ONLY error handling.""" response = await agent.generate_str("Calculate 10 / 0") await session.assert_that( Expect.judge.llm("Handles division by zero appropriately"), response=response ) ``` **When to go broader:** Complex agent behaviors sometimes require end-to-end scenarios (multi-tool flows, recovery, efficiency). In those cases: * Keep assertions layered and named (content, tools, performance, judge) * Bound scope (one coherent workflow per test) * Use separate tests for alternative branches or failure paths Example end-to-end scenario: ```python @task("Fetch and summarize workflow") async def test_document_flow(agent, session): # Single coherent workflow summary = await agent.generate_str( "Fetch https://example.com and summarize the main content" ) await session.assert_that(Expect.tools.was_called("fetch"), name="fetched") await session.assert_that( Expect.content.contains("Example Domain"), response=summary, name="has_title" ) await session.assert_that(Expect.performance.max_iterations(3), name="efficient") await session.assert_that( Expect.path.efficiency(expected_tool_sequence=["fetch"], allow_extra_steps=0), name="golden_path", ) ``` ```python # BAD: Testing too many things at once @task("Test everything") async def test_calculator_everything(agent, session): response = await agent.generate_str( "Calculate 5+3, then 10/0, then fetch weather, " "then validate JSON, then check performance" ) # This test is hard to debug when it fails! ``` ### 2. Use descriptive names **✅ Do:** Name tests to describe what they verify ```python @task("Should return error message when dividing by zero") async def test_division_by_zero_returns_error(agent, session): # Clear what this test checks pass @task("Should complete simple calculation in under 2 seconds") async def test_simple_calculation_performance(agent, session): # Performance expectation is clear pass ``` **❌ Don't:** Use vague or generic names ```python # BAD: What does this test? @task("Test 1") async def test_1(agent, session): pass # BAD: Too generic @task("Calculator test") async def test_calc(agent, session): pass ``` ### 3. Make tests independent **✅ Do:** Each test should run in isolation ```python @task("Test user creation") async def test_create_user(agent, session): """Creates its own test data.""" user_id = f"test_user_{uuid.uuid4()}" response = await agent.generate_str( f"Create user with ID {user_id}" ) # Clean up after ourselves await agent.generate_str(f"Delete user {user_id}") ``` **❌ Don't:** Depend on other tests or shared state ```python # BAD: Depends on previous test @task("Test user update") async def test_update_user(agent, session): # Assumes user was created by another test! response = await agent.generate_str( "Update user test_user_123" # Will fail if run alone ) ``` ### 4. Use explicit assertions **✅ Do:** Be specific about expectations ```python @task("Test JSON response format") async def test_json_format(agent, session): response = await agent.generate_str("Get user data as JSON") # Explicit, specific assertions await session.assert_that( Expect.content.regex(r'\{"id":\s*\d+'), # Has ID field Expect.content.regex(r'"name":\s*"[^"]+"'), # Has name Expect.content.regex(r'"created":\s*"\d{4}-\d{2}-\d{2}"'), # Has date response=response ) ``` **❌ Don't:** Use vague or implicit checks ```python # BAD: Too vague await session.assert_that( Expect.content.contains("data"), # What data? response=response ) ``` ## Test organization ### Directory structure Organize tests by functionality and type: ``` tests/ ├── unit/ # Fast, isolated tests │ ├── test_calculator.py │ ├── test_validator.py │ └── test_parser.py ├── integration/ # Multi-component tests │ ├── test_data_pipeline.py │ ├── test_api_workflow.py │ └── test_database_sync.py ├── e2e/ # End-to-end scenarios │ ├── test_user_journey.py │ └── test_complete_workflow.py ├── performance/ # Performance tests │ ├── test_response_time.py │ └── test_throughput.py ├── fixtures/ # Shared test data │ ├── sample_data.json │ └── test_configs.yaml └── conftest.py # Shared fixtures and setup ``` ### Test file naming Follow consistent naming patterns: ```python # Good naming patterns test__.py # Examples: test_calculator_basic_operations.py test_calculator_error_handling.py test_calculator_performance.py test_api_authentication.py test_api_rate_limiting.py ``` ### Grouping related tests Use classes or modules to group related tests: ```python # tests/test_calculator_operations.py class TestBasicOperations: """All basic math operations.""" @task("Addition") async def test_addition(self, agent, session): pass @task("Subtraction") async def test_subtraction(self, agent, session): pass class TestAdvancedOperations: """Scientific calculator features.""" @task("Square root") async def test_sqrt(self, agent, session): pass @task("Logarithm") async def test_log(self, agent, session): pass ``` ## Assertion strategies ### Layered assertions Build assertions from deterministic to probabilistic: ```python @task("Test comprehensive response quality") async def test_response_quality(agent, session): response = await agent.generate_str( "Explain how to reset a password" ) # Layer 1: Structural (deterministic) await session.assert_that( Expect.content.contains("password"), Expect.content.contains("reset"), response=response ) # Layer 2: Tool usage (deterministic) await session.assert_that( Expect.tools.was_called("help_system"), Expect.tools.success_rate(min_rate=1.0) ) # Layer 3: Performance (measurable) await session.assert_that( Expect.performance.response_time_under(3000), Expect.performance.max_iterations(2) ) # Layer 4: Quality (probabilistic) await session.assert_that( Expect.judge.llm( "Provides clear, step-by-step instructions", min_score=0.8 ), response=response ) ``` ### Assertion selection guide Choose assertions based on what you're testing: | Testing | Use These Assertions | Avoid | | ------------------ | --------------------------------------- | -------------------------------- | | **Correctness** | `contains`, `regex` | LLM judges for exact values | | **Tool Usage** | `was_called`, `called_with`, `sequence` | Content checks for tool behavior | | **Performance** | `response_time_under`, `max_iterations` | Exact timing matches | | **Quality** | `judge.llm`, `multi_criteria` | Brittle string matching | | **Error Handling** | `judge.llm` with error rubric | Expecting exact error text | ### Custom assertion patterns Create reusable assertion combinations: ```python # assertions/common.py async def assert_successful_api_call(session, response, endpoint): """Reusable assertion for API calls.""" await session.assert_that( Expect.tools.was_called("http_client"), Expect.tools.success_rate(min_rate=1.0), Expect.content.regex(r'"status":\s*20\d'), # 2xx status Expect.content.contains(endpoint), response=response ) # Use in tests @task("Test user API") async def test_user_api(agent, session): response = await agent.generate_str("Get user data from /api/users") await assert_successful_api_call(session, response, "/api/users") ``` ## Performance optimization ### Minimize LLM calls **✅ Do:** Batch operations when possible ```python @task("Test multiple calculations efficiently") async def test_batch_calculations(agent, session): # One LLM call for multiple operations response = await agent.generate_str(""" Calculate the following: 1. 15 + 27 2. 98 - 43 3. 12 * 8 """) # Verify all results for expected in ["42", "55", "96"]: await session.assert_that( Expect.content.contains(expected), response=response ) ``` **❌ Don't:** Make unnecessary separate calls ```python # BAD: Three separate LLM calls response1 = await agent.generate_str("Calculate 15 + 27") response2 = await agent.generate_str("Calculate 98 - 43") response3 = await agent.generate_str("Calculate 12 * 8") ``` ### Use appropriate models Match model to test complexity: ```python # conftest.py or setup def get_model_for_test_type(test_type): """Select appropriate model for test type.""" models = { "simple": "claude-3-haiku-20240307", # Fast, cheap "complex": "claude-3-5-sonnet-20241022", # Capable "judge": "claude-3-5-sonnet-20241022", # Accurate judging } return models.get(test_type, "claude-3-haiku-20240307") ``` ### Parallel execution Run independent tests concurrently: ```yaml # mcpeval.yaml execution: max_concurrency: 10 # Run up to 10 tests in parallel parallel: true ``` ```python # Mark tests that can run in parallel @pytest.mark.parallel @task("Independent test 1") async def test_independent_1(agent, session): pass @pytest.mark.parallel @task("Independent test 2") async def test_independent_2(agent, session): pass ``` ### Cache when appropriate ```yaml # mcpeval.yaml cache: enabled: true ttl: 3600 # Cache for 1 hour during development development: cache_responses: true # Cache LLM responses ``` ## Reliability patterns ### Handle non-determinism LLMs are probabilistic, so account for variation: ```python @task("Test with retry logic") @retry(max_attempts=3) async def test_with_variation(agent, session): """Retry on transient failures.""" response = await agent.generate_str( "Generate a creative story about testing" ) # Use flexible assertions await session.assert_that( Expect.judge.llm( "Story is about testing and is creative", min_score=0.7 # Allow some variation ), response=response ) ``` ### Reduce flakiness Common causes and solutions: | Flakiness Source | Solution | | ----------------- | ------------------------------------------ | | Network issues | Add retries, increase timeouts | | Race conditions | Use explicit waits, not sleep | | Random data | Use fixed seeds or deterministic data | | External services | Mock or use test instances | | LLM variation | Lower temperature, use flexible assertions | ```python # Reduce LLM variation response = await agent.generate_str( prompt, temperature=0, # Deterministic seed=42 # Fixed seed if supported ) ``` ### Test isolation Ensure tests don't affect each other: ```python @setup def reset_test_environment(): """Clean state before each test.""" # Clear any caches cache.clear() # Reset any global state global_state.reset() # Ensure clean database db.rollback() @teardown def cleanup_test_artifacts(): """Clean up after each test.""" # Delete test files for file in Path("test_outputs").glob("test_*"): file.unlink() # Close connections await close_all_connections() ``` ## Maintainability ### Documentation in tests Document complex test logic: ```python @task("Test complex data transformation workflow") async def test_data_transformation(agent, session): """ Test the complete data transformation pipeline. Flow: 1. Load raw CSV data 2. Validate format and content 3. Transform to normalized JSON 4. Store in database 5. Generate summary report Expected behavior: - All steps complete successfully - Data integrity is maintained - Report contains key metrics """ # Step 1: Load data # Important: Using test fixture with known values response = await agent.generate_str( "Process data from test_fixtures/sample.csv" ) # Verify each step completed await session.assert_that( Expect.tools.sequence([ "file_reader", "validator", "transformer", "database", "report_generator" ]), name="correct_pipeline_sequence" ) ``` ### Parameterized test patterns Make tests reusable with parameters: ```python class TestServerResponses: """Test various server response scenarios.""" @parametrize("status_code,expected_behavior", [ (200, "processes normally"), (404, "reports not found"), (500, "handles server error"), (429, "respects rate limit"), ]) @task("Test HTTP status {status_code} handling") async def test_status_handling( self, agent, session, status_code, expected_behavior ): response = await agent.generate_str( f"Handle HTTP {status_code} response" ) await session.assert_that( Expect.judge.llm(f"Agent {expected_behavior}"), response=response ) ``` ### Test data management Centralize test data: ```python # test_data/datasets.py class TestDatasets: """Centralized test data management.""" @staticmethod def get_user_data(variant="default"): """Get test user data.""" datasets = { "default": {"id": 1, "name": "Test User"}, "invalid": {"id": "not_a_number"}, "large": {"id": 999999, "name": "x" * 1000}, } return datasets.get(variant, datasets["default"]) @staticmethod def get_calculation_cases(): """Get calculation test cases.""" return [ ("5 + 3", "8"), ("10 - 4", "6"), ("3 * 7", "21"), ("20 / 4", "5"), ] ``` ### Version your tests Track test evolution with your code: ```python @task("Test API v2 compatibility") @since_version("2.0.0") async def test_api_v2(agent, session): """Test new v2 API features.""" pass @task("Test legacy API support") @deprecated("3.0.0", "Use test_api_v2 instead") async def test_api_v1(agent, session): """Test old API for backwards compatibility.""" pass ``` ## Anti-patterns to avoid ### 1. Testing implementation details **❌ Don't:** Test internal implementation ```python # BAD: Testing internal state response = await agent.generate_str("Calculate something") # Don't check internal variables or private methods assert agent._internal_state == "some_value" # Bad! ``` **✅ Do:** Test behavior and outputs ```python # GOOD: Test observable behavior response = await agent.generate_str("Calculate 5 + 3") await session.assert_that( Expect.content.contains("8"), response=response ) ``` ### 2. Overusing LLM judges **❌ Don't:** Use judges for deterministic checks ```python # BAD: Using judge for exact value await session.assert_that( Expect.judge.llm("Response contains exactly '42'"), response=response ) ``` **✅ Do:** Use appropriate assertion types ```python # GOOD: Direct assertion for exact values await session.assert_that( Expect.content.contains("42"), response=response ) ``` ### 3. Ignoring test failures **❌ Don't:** Skip or ignore failing tests ```python # BAD: Ignoring failures @pytest.mark.skip("Fails sometimes") # Don't ignore! async def test_important_feature(agent, session): pass ``` **✅ Do:** Fix or properly mark flaky tests ```python # GOOD: Fix the root cause or mark appropriately @pytest.mark.flaky(reruns=3, reruns_delay=2) async def test_with_external_dependency(agent, session): """Test that depends on external service.""" pass ``` ### 4. Magic numbers and strings **❌ Don't:** Use unexplained values ```python # BAD: What do these numbers mean? await session.assert_that( Expect.performance.response_time_under(5000), # Why 5000? Expect.judge.llm("Good", min_score=0.73) # Why 0.73? ) ``` **✅ Do:** Use named constants with explanations ```python # GOOD: Clear, documented values MAX_ACCEPTABLE_RESPONSE_TIME_MS = 5000 # SLA requirement QUALITY_THRESHOLD = 0.75 # Based on user study baseline await session.assert_that( Expect.performance.response_time_under(MAX_ACCEPTABLE_RESPONSE_TIME_MS), Expect.judge.llm("Meets quality standards", min_score=QUALITY_THRESHOLD) ) ``` ## Testing checklist Use this checklist for every test you write: * [ ] **Single purpose** - Tests one specific behavior * [ ] **Descriptive name** - Clearly indicates what's being tested * [ ] **Independent** - Doesn't depend on other tests * [ ] **Deterministic** - Produces consistent results * [ ] **Fast** - Runs quickly (\< 5 seconds for unit tests) * [ ] **Documented** - Has docstring explaining purpose * [ ] **Maintainable** - Easy to understand and modify * [ ] **Appropriate assertions** - Uses right assertion types * [ ] **Error handling** - Handles expected failures gracefully * [ ] **Cleanup** - Cleans up any created resources ## Advanced patterns ### Property-based testing Test properties rather than specific examples: ```python from hypothesis import given, strategies as st @given( a=st.integers(min_value=-1000, max_value=1000), b=st.integers(min_value=-1000, max_value=1000) ) @task("Test addition properties") async def test_addition_properties(agent, session, a, b): """Test mathematical properties of addition.""" response = await agent.generate_str(f"Calculate {a} + {b}") # Verify commutative property response2 = await agent.generate_str(f"Calculate {b} + {a}") # Both should have the same result result1 = extract_number(response) result2 = extract_number(response2) assert result1 == result2, "Addition should be commutative" ``` ### Contract testing Define contracts between components: ```python @task("Test API contract") async def test_api_contract(agent, session): """Verify API adheres to contract.""" response = await agent.generate_str("Get user from API") # Verify response structure matches contract contract = { "id": int, "name": str, "email": str, "created_at": str, } for field, expected_type in contract.items(): await session.assert_that( Expect.content.contains(f'"{field}"'), name=f"has_{field}_field", response=response ) ``` ### Mutation testing Verify your tests catch bugs: ```python @task("Test catches calculation errors") async def test_mutation_detection(agent, session): """Verify test suite detects bugs.""" # Introduce intentional bug with mock.patch("calculator.add", return_value=99): response = await agent.generate_str("Calculate 5 + 3") # This should fail, proving our test catches bugs with pytest.raises(AssertionError): await session.assert_that( Expect.content.contains("8"), response=response ) ``` ## Continuous improvement ### Metrics to track Monitor your test suite health: * **Pass rate** - Should be > 95% for stable tests * **Execution time** - Track trends, investigate increases * **Flakiness** - Identify and fix flaky tests * **Coverage** - Ensure critical paths are tested * **Maintenance cost** - Time spent fixing tests ### Regular reviews Schedule periodic test suite reviews: 1. **Weekly:** Review failed tests, fix or mark as flaky 2. **Monthly:** Remove obsolete tests, update assertions 3. **Quarterly:** Refactor test organization, update patterns 4. **Yearly:** Major test suite health assessment {/* TODO: Add dashboard screenshot showing test suite health metrics */} *** **You're now equipped** with best practices that will make your mcp-eval tests reliable, maintainable, and valuable! Remember: good tests are an investment in your project's future. 🌟 # Building with Claude Source: https://mcp-eval.ai/building-with-claude Use Claude subagents to accelerate mcp-eval test development with specialized AI assistants mcp-eval includes specialized Claude subagents that help you write, debug, and optimize tests. These subagents are AI assistants with deep knowledge of mcp-eval patterns and best practices. ## Available Subagents mcp-eval ships with several specialized subagents in [`src/mcp_eval/data/subagents/`](https://github.com/lastmile-ai/mcp-eval/tree/main/src/mcp_eval/data/subagents). You can view and copy the complete definitions below. Save these as `.md` files in your `.claude/agents` directory: ### Test Writer View the complete [MCP-Eval Test Writer](/subagents/mcp-eval-test-writer) subagent definition. Expert at writing comprehensive mcp-eval tests in all styles (decorator, pytest, dataset). ### Test Generator View the complete [MCP-Eval Test Generator](/subagents/mcp-eval-test-generator) subagent definition. Generates complete test suites with diverse scenarios and comprehensive coverage. ### Debugger View the complete [MCP-Eval Debugger](/subagents/mcp-eval-debugger) subagent definition. Expert at debugging test failures, analyzing OTEL traces, and troubleshooting configuration issues. ### Config Expert View the complete [MCP-Eval Config Expert](/subagents/mcp-eval-config-expert) subagent definition. Expert at configuring mcp-eval and managing mcpeval.yaml files for optimal performance. ### Scenario Designer View the complete [Test Scenario Designer](/subagents/test-scenario-designer) subagent definition. Creates diverse, high-quality test scenarios for MCP servers (Step 1 of generation pipeline). ### Assertion Refiner View the complete [Test Assertion Refiner](/subagents/test-assertion-refiner) subagent definition. Refines and enhances test assertions for comprehensive coverage (Step 2 of generation pipeline). ### Code Emitter View the complete [Test Code Emitter](/subagents/test-code-emitter) subagent definition. Converts test scenarios into valid Python test code with proper syntax (Step 3 of generation pipeline). ## Setup For Claude Code: ```bash mkdir -p .claude/agents cp path/to/mcp_eval/data/subagents/*.md .claude/agents/ ``` Find your package location: ```bash python -c "import mcp_eval, os; print(os.path.join(os.path.dirname(mcp_eval.__file__), 'data', 'subagents'))" ``` Add to your `mcpeval.yaml`: ```yaml agents: enabled: true search_paths: # Add the path from the command above - "/path/to/site-packages/mcp_eval/data/subagents" # Standard locations - ".claude/agents" - "~/.claude/agents" pattern: "*.md" ``` If running from source: ```yaml agents: enabled: true search_paths: - "./src/mcp_eval/data/subagents" # See: https://github.com/lastmile-ai/mcp-eval/tree/main/src/mcp_eval/data/subagents pattern: "*.md" ``` ## Using Subagents in Claude Code Once configured, Claude Code will automatically discover and use these subagents when appropriate. You can also explicitly request them: ### Writing Tests ``` "Use the mcp-eval-test-writer subagent to create comprehensive tests for my fetch server" ``` ### Debugging Failures ``` "Use the mcp-eval-debugger subagent to help me understand why my tests are failing" ``` ### Configuration Help ``` "Use the mcp-eval-config-expert subagent to set up my mcpeval.yaml correctly" ``` ## Using Subagents for Test Generation The test generation subagents work together to create high-quality tests: 1. **test-scenario-designer** - Designs comprehensive test scenarios 2. **test-assertion-refiner** - Enhances assertions for better coverage 3. **test-code-emitter** - Generates syntactically correct Python code These can be used manually or integrated into the `mcp-eval generate` workflow. ## Subagent Examples ### Test Writer Example The `mcp-eval-test-writer` subagent can help create tests in any style: ```python # Decorator style @task("Fetch and validate") async def test_fetch_validate(agent: Agent, session: Session): response = await agent.generate_str("Fetch example.com") await session.assert_that( Expect.tools.was_called("fetch"), response=response ) ``` ```python # Pytest style @pytest.mark.asyncio async def test_fetch_with_error(mcp_agent, mcp_session): response = await mcp_agent.generate_str("Fetch invalid-url") await mcp_session.assert_that( Expect.content.contains("error"), response=response ) ``` ### Debugger Example The `mcp-eval-debugger` helps diagnose issues: * Analyzes OTEL traces to find performance bottlenecks * Identifies assertion failures and suggests fixes * Troubleshoots configuration problems * Explains error messages and stack traces ### Config Expert Example The `mcp-eval-config-expert` helps with configuration: ```yaml # Optimized configuration for parallel execution execution: max_concurrency: 10 timeout_seconds: 60 fail_fast: true agents: definitions: - name: "fetch_agent" provider: anthropic model: claude-3-5-sonnet-20241022 instruction: "You are a helpful assistant that can fetch URLs" server_names: ["fetch"] ``` ## Best Practices 1. **Use the right subagent for the task** - Each subagent is specialized for specific aspects of mcp-eval 2. **Combine subagents** - Use multiple subagents together for complex tasks 3. **Provide context** - Give subagents information about your server's capabilities 4. **Review generated code** - Subagents provide excellent starting points, but review and customize as needed 5. **Keep subagents updated** - Pull the latest mcp-eval version for improved subagents ## Integration with mcp-agent If you're using [mcp-agent](https://github.com/modelcontextprotocol/mcp-agent), these subagents are compatible with its agent loading system. Configure your `mcp-agent.config.yaml` to include the mcp-eval subagents search path. ## Contributing Subagents To contribute new subagents: 1. Create a markdown file following the format in [`src/mcp_eval/data/subagents/`](https://github.com/lastmile-ai/mcp-eval/tree/main/src/mcp_eval/data/subagents) 2. Include the frontmatter with name, description, and tools 3. Write clear instructions for the subagent's expertise 4. Test the subagent with real mcp-eval tasks 5. Submit a pull request ## Related Resources * [Generating Tests with LLMs](/test-generation) - Automated test generation * [Agent Configuration](/agents) - Configure agents for testing * [Best Practices](/best-practices) - General mcp-eval best practices * [GitHub: Subagents Source](https://github.com/lastmile-ai/mcp-eval/tree/main/src/mcp_eval/data/subagents) - View all subagent definitions on GitHub # Changelog Source: https://mcp-eval.ai/changelog High-level changes and user‑visible updates. Coming soon. # CI/CD Source: https://mcp-eval.ai/ci-cd Run mcp-eval in GitHub Actions, publish artifacts, post PR comments, and add badges. ### Run action The recommended approach is to use the reusable workflow which handles all the setup, testing, and deployment. For the complete workflow configuration, visit [mcpeval.yml](https://github.com/lastmile-ai/mcp-eval/blob/main/.github/workflows/mcpeval.yml). ```yaml name: MCP-Eval CI on: push: branches: [main, master, trunk] workflow_dispatch: jobs: call-mcpeval: uses: lastmile-ai/mcp-eval/.github/workflows/mcpeval-reusable.yml with: deploy-pages: true permissions: contents: read pages: write id-token: write pull-requests: write secrets: inherit ``` Alternatively, you can directly use the action in your workflow: ```yaml steps: - name: Checkout uses: actions/checkout@v4 - name: Run MCP-Eval (uv) id: mcpeval uses: lastmile-ai/mcp-eval/.github/actions/mcp-eval/run with: python-version: "3.11" working-directory: . run-args: "-v" tests: tests/ reports-dir: mcpeval-reports json-report: mcpeval-results.json markdown-report: mcpeval-results.md html-report: mcpeval-results.html artifact-name: mcpeval-artifacts pr-comment: "true" set-summary: "true" upload-artifacts: "true" env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: Generate and upload badges uses: lastmile-ai/mcp-eval/.github/actions/mcp-eval/badges with: report-path: ${{ steps.mcpeval.outputs.results-json-path }} output-dir: badges format: "both" upload-artifacts: "true" artifact-name: mcpeval-badges ``` Sources: * Action: [action.yaml](https://github.com/lastmile-ai/mcp-eval/blob/main/.github/actions/mcp-eval/run/action.yaml) * README: [run/README.md](https://github.com/lastmile-ai/mcp-eval/blob/main/.github/actions/mcp-eval/run/README.md) * Workflows: [mcpeval.yml](https://github.com/lastmile-ai/mcp-eval/blob/main/.github/workflows/mcpeval.yml), [mcpeval-reusable.yml](https://github.com/lastmile-ai/mcp-eval/blob/main/.github/workflows/mcpeval-reusable.yml) ### Publish HTML and Badges via Pages The workflow automatically deploys both the HTML report and badges to GitHub Pages when pushing to main/master branches. After deployment, badges are accessible at `https://.github.io//badges/`. To enable GitHub Pages deployment: 1. Enable GitHub Pages in your repository settings 2. The workflow will automatically deploy on pushes to main/master 3. Badges and reports will be available at your Pages URL ### Badges After deployment to GitHub Pages, reference badges using your Pages URL: ```markdown [![mcp-tests](https://img.shields.io/endpoint?url=https://YOUR_USERNAME.github.io/YOUR_REPO/badges/mcp-tests.json&cacheSeconds=300)](https://YOUR_USERNAME.github.io/YOUR_REPO/) [![mcp-cov](https://img.shields.io/endpoint?url=https://YOUR_USERNAME.github.io/YOUR_REPO/badges/mcp-cov.json&cacheSeconds=300)](https://YOUR_USERNAME.github.io/YOUR_REPO/) ``` For example, [![mcp-tests](https://img.shields.io/endpoint?url=https://lastmile-ai.github.io/mcp-eval/badges/mcp-tests.json\&cacheSeconds=300)](https://lastmile-ai.github.io/mcp-eval/) [![mcp-cov](https://img.shields.io/endpoint?url=https://lastmile-ai.github.io/mcp-eval/badges/mcp-cov.json\&cacheSeconds=300)](https://lastmile-ai.github.io/mcp-eval/) {/* TODO: Add screenshots of the PR comment, the summary, and the published HTML report on Pages. */} # CLI Reference Source: https://mcp-eval.ai/cli-reference Complete reference for MCP-Eval command-line interface, including commands and flags # CLI reference > Complete reference for MCP-Eval command-line interface, including commands and flags. ## CLI commands | Command | Description | Example | | :--------------------- | :-------------------------------------- | :------------------------------------- | | `mcp-eval init` | Initialize a new MCP-Eval project | `mcp-eval init` | | `mcp-eval generate` | Generate test scenarios for MCP servers | `mcp-eval generate --style pytest` | | `mcp-eval run` | Execute test files and datasets | `mcp-eval run tests/` | | `mcp-eval dataset` | Run dataset evaluation | `mcp-eval dataset datasets/basic.yaml` | | `mcp-eval validate` | Validate configuration | `mcp-eval validate --quick` | | `mcp-eval doctor` | Diagnose setup issues | `mcp-eval doctor --full` | | `mcp-eval issue` | Create GitHub issue with diagnostics | `mcp-eval issue --title "Bug report"` | | `mcp-eval server add` | Add MCP server to config | `mcp-eval server add` | | `mcp-eval server list` | List configured servers | `mcp-eval server list -v` | | `mcp-eval agent add` | Add test agent to config | `mcp-eval agent add` | | `mcp-eval agent list` | List configured agents | `mcp-eval agent list --name default` | | `mcp-eval version` | Show version information | `mcp-eval version` | ## Setup & Configuration ### init Initialize a new MCP-Eval project with interactive setup. ```bash mcp-eval init [OPTIONS] ``` | Flag | Description | Default | Example | | :----------- | :--------------------------------------------- | :------ | :----------------------- | | `--out-dir` | Project directory for configs | `.` | `--out-dir ./my-project` | | `--template` | Bootstrap template: `empty`, `basic`, `sample` | `basic` | `--template sample` | **What it does:** * Creates `mcpeval.yaml` and `mcpeval.secrets.yaml` * Prompts for LLM provider and API key * Auto-detects and imports servers from `.cursor/mcp.json` or `.vscode/mcp.json` * Configures default agent with instructions * Sets up judge configuration for test evaluation Source: [generator.py:841-1015](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/cli/generator.py) ### server add Add MCP server to configuration. ```bash mcp-eval server add [OPTIONS] ``` | Flag | Description | Default | Example | | :---------------- | :------------------------ | :------ | :--------------------------------- | | `--out-dir` | Project directory | `.` | `--out-dir ./project` | | `--from-mcp-json` | Import from mcp.json file | - | `--from-mcp-json .cursor/mcp.json` | | `--from-dxt` | Import from DXT file | - | `--from-dxt manifest.dxt` | Source: [generator.py:1529-1633](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/cli/generator.py) ### agent add Add test agent configuration. ```bash mcp-eval agent add [OPTIONS] ``` | Flag | Description | Default | Example | | :---------- | :---------------- | :------ | :-------------------- | | `--out-dir` | Project directory | `.` | `--out-dir ./project` | Source: [generator.py:1635-1701](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/cli/generator.py) ## Test Generation ### generate Generate test scenarios and write test files for MCP servers. ```bash mcp-eval generate [OPTIONS] ``` | Flag | Description | Default | Example | | :------------- | :---------------------------------------------------- | :------ | :------------------------------- | | `--out-dir` | Project directory | `.` | `--out-dir ./tests` | | `--style` | Test format: `pytest`, `decorators`, `dataset` | - | `--style pytest` | | `--n-examples` | Number of scenarios to generate | `6` | `--n-examples 10` | | `--provider` | LLM provider: `anthropic`, `openai` | - | `--provider anthropic` | | `--model` | Specific model to use | - | `--model claude-3-opus-20240229` | | `--verbose` | Show detailed error messages | `False` | `--verbose` | | `--output` | Explicit output file path | - | `--output tests/custom.py` | | `--update` | Append tests to existing file instead of creating new | - | `--update tests/test.py` | **What it does:** * Discovers server tools via MCP protocol * Generates test scenarios with AI * Refines assertions for each scenario * Validates generated Python code * Outputs test files or datasets Source: [generator.py:1017-1346](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/cli/generator.py) **Update mode:** When using `--update`, the command appends new tests to an existing file rather than creating a new one. The file path provided to `--update` becomes the target file. Example: ```bash # Append 5 new tests to existing file mcp-eval generate --update tests/test_server.py --n-examples 5 ``` ## Test Execution ### run Execute test files and generate reports. ```bash mcp-eval run [OPTIONS] ``` | Flag | Description | Default | Example | | :------------------ | :----------------------- | :------ | :---------------------- | | `-v, --verbose` | Detailed output | `False` | `-v` | | `--json` | Output JSON report | - | `--json results.json` | | `--markdown` | Output Markdown report | - | `--markdown results.md` | | `--html` | Output HTML report | - | `--html results.html` | | `--max-concurrency` | Parallel execution limit | - | `--max-concurrency 4` | **Accepts all standard pytest options** Source: [runner.py](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/runner.py) ### dataset Run dataset evaluation. ```bash mcp-eval dataset [OPTIONS] ``` Same options as `run` command. Source: [runner.py](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/runner.py) ## Inspection & Validation ### server list List configured MCP servers. ```bash mcp-eval server list [OPTIONS] ``` | Flag | Description | Default | Example | | :-------------- | :---------------- | :------ | :------------------------ | | `--project-dir` | Project directory | `.` | `--project-dir ./project` | | `-v, --verbose` | Show full details | `False` | `-v` | Source: [list\_command.py:20-102](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/cli/list_command.py) ### agent list List configured agents. ```bash mcp-eval agent list [OPTIONS] ``` | Flag | Description | Default | Example | | :-------------- | :--------------------- | :------ | :------------------------ | | `--project-dir` | Project directory | `.` | `--project-dir ./project` | | `-v, --verbose` | Show full instructions | `False` | `-v` | | `--name` | Show specific agent | - | `--name default` | Source: [list\_command.py:104-185](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/cli/list_command.py) ### validate Validate MCP-Eval configuration and connections. ```bash mcp-eval validate [OPTIONS] ``` | Flag | Description | Default | Example | | :----------------------- | :-------------------- | :------ | :------------------------ | | `--project-dir` | Project directory | `.` | `--project-dir ./project` | | `--servers/--no-servers` | Validate servers | `True` | `--no-servers` | | `--agents/--no-agents` | Validate agents | `True` | `--no-agents` | | `--quick` | Skip connection tests | `False` | `--quick` | **What it checks:** * API keys are configured * Judge model is set * Servers can be connected to * Agents reference valid servers * LLM connections work Source: [validate.py:342-514](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/cli/validate.py) ## Debugging & Diagnostics ### doctor Comprehensive diagnostics for troubleshooting. ```bash mcp-eval doctor [OPTIONS] ``` | Flag | Description | Default | Example | | :-------------- | :----------------------- | :------ | :------------------------ | | `--project-dir` | Project directory | `.` | `--project-dir ./project` | | `--full` | Include connection tests | `False` | `--full` | **What it checks:** * Python version and packages * Configuration files * Environment variables * System information * Recent test errors * Provides fix suggestions Source: [doctor.py](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/cli/doctor.py) ### issue Create GitHub issues with diagnostic information. ```bash mcp-eval issue [OPTIONS] ``` | Flag | Description | Default | Example | | :--------------------- | :----------------- | :------ | :----------------------------- | | `--project-dir` | Project directory | `.` | `--project-dir ./project` | | `--title` | Issue title | - | `--title "Connection timeout"` | | `--no-include-outputs` | Skip test outputs | `False` | `--no-include-outputs` | | `--no-open-browser` | Don't open browser | `False` | `--no-open-browser` | Source: [issue.py](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/cli/issue.py) ### version Show version information. ```bash mcp-eval version ``` Source: [**init**.py:34-42](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/cli/__init__.py) ## Configuration Files MCP-Eval uses two primary configuration files: ### mcpeval.yaml Main configuration containing: * Server definitions (transport, command, args, env) * Agent definitions (name, instruction, server\_names) * Judge configuration (provider, model, min\_score) * Default agent setting * Reporting configuration ### mcpeval.secrets.yaml Sensitive configuration containing: * API keys for LLM providers * Authentication tokens * Other secrets Both files are created by `mcp-eval init` and can be edited manually. ## Environment Variables MCP-Eval respects these environment variables: | Variable | Description | Example | | :------------------ | :----------------------- | :---------------------- | | `ANTHROPIC_API_KEY` | Anthropic Claude API key | `sk-ant-...` | | `OPENAI_API_KEY` | OpenAI API key | `sk-...` | | `GOOGLE_API_KEY` | Google API key | `...` | | `COHERE_API_KEY` | Cohere API key | `...` | | `AZURE_API_KEY` | Azure OpenAI API key | `...` | | `MCPEVAL_CONFIG` | Path to config file | `./config/mcpeval.yaml` | | `MCPEVAL_SECRETS` | Path to secrets file | `./config/secrets.yaml` | ## Typical Workflow ### 1. Initialize Project ```bash # Create new project mcp-eval init # Or with sample files mcp-eval init --template sample ``` ### 2. Configure Servers & Agents ```bash # Add server interactively mcp-eval server add # Import from existing config mcp-eval server add --from-mcp-json .cursor/mcp.json # Add test agent mcp-eval agent add # List what's configured mcp-eval server list -v mcp-eval agent list ``` ### 3. Validate Setup ```bash # Full validation with connections mcp-eval validate # Quick config check only mcp-eval validate --quick # Diagnose issues mcp-eval doctor --full ``` ### 4. Generate Tests ```bash # Generate pytest tests mcp-eval generate --style pytest --n-examples 10 # Add more tests to existing file mcp-eval generate --update tests/test_server.py --n-examples 5 # Generate dataset for batch testing mcp-eval generate --style dataset ``` ### 5. Execute Tests ```bash # Run all tests mcp-eval run tests/ # Run specific test with reports mcp-eval run tests/test_fetch.py -v --json report.json --markdown report.md # Run dataset mcp-eval dataset datasets/basic.yaml ``` ### 6. Debug Issues ```bash # If tests fail mcp-eval doctor # Create GitHub issue mcp-eval issue --title "Tests failing with timeout" ``` ## Test Styles MCP-Eval supports three test formats: ### pytest Standard pytest format with test functions and assertions. Best for integration with existing Python test suites. ### decorators MCP-Eval's decorator-based format using `@task` and `@setup`. Provides rich async support and session management. ### dataset YAML-based test cases for batch evaluation. Ideal for non-programmers and test data management. ## See also * [Quickstart guide](quickstart) - Getting started with MCP-Eval * [Test Generation](generation) - Generating tests with AI * [Writing Tests](writing-tests) - Manual test creation * [Configuration](configuration) - Detailed configuration options * [GitHub Repository](https://github.com/lastmile-ai/mcp-eval) - Source code and issues # Common Workflows Source: https://mcp-eval.ai/common-workflows Step-by-step guides for typical mcp-eval tasks, from writing your first test to CI/CD integration. > Learn proven patterns for testing MCP servers and agents. Each workflow includes practical examples and tips from real-world usage. ## Write your first test Pick the style that fits your workflow: **Decorator** (simplest): ```python from mcp_eval import task, Expect @task("My first test") async def test_basic(agent, session): response = await agent.generate_str("Hello") await session.assert_that(Expect.content.contains("Hi")) ``` **Pytest** (familiar): ```python @pytest.mark.asyncio async def test_basic(mcp_agent): response = await mcp_agent.generate_str("Hello") assert "Hi" in response ``` Start simple, then add more specific checks: ```python # Start with content await session.assert_that(Expect.content.contains("success")) # Add tool verification await session.assert_that(Expect.tools.was_called("my_tool")) # Include performance await session.assert_that(Expect.performance.response_time_under(5000)) ``` ```bash # Run with verbose output mcp-eval run test_basic.py -v # Generate reports mcp-eval run test_basic.py --html report.html ``` Review failures, adjust assertions, and rerun. Start with simple content assertions, then gradually add tool and performance checks as you understand your system's behavior. ## Test an MCP server comprehensively List all tools your server provides: ```bash mcp-eval server list --verbose ``` Note the available tools and their expected behaviors. Write tests covering normal and edge cases: ```python @task("Test calculator add - normal case") async def test_add_normal(agent, session): response = await agent.generate_str("Calculate 5 + 3") await session.assert_that(Expect.tools.was_called("add")) await session.assert_that(Expect.content.contains("8")) @task("Test calculator add - edge case") async def test_add_overflow(agent, session): response = await agent.generate_str("Calculate 999999999 + 999999999") await session.assert_that(Expect.tools.was_called("add")) # Check for appropriate handling await session.assert_that( Expect.content.regex(r"(overflow|large|error)", case_sensitive=False) ) ``` Verify graceful failure: ```python @task("Test invalid input handling") async def test_error_handling(agent, session): response = await agent.generate_str("Calculate abc + xyz") # Should either fail gracefully or explain the issue await session.assert_that( Expect.content.regex(r"(invalid|error|cannot|unable)") ) ``` For systematic testing: ```python from mcp_eval import Dataset, Case dataset = Dataset( name="Calculator Server Tests", cases=[ Case( name="addition", inputs="Calculate 10 + 20", expected_output="30", evaluators=[ ToolWasCalled("add"), ResponseContains("30") ] ), # Add more cases... ] ) ``` ## Create and enforce a golden path Ensure your agent follows the optimal execution path: Identify the minimal, correct sequence of tools: ```python # Example: validate → process → format golden_path = ["validate_input", "process_data", "format_output"] ``` ```python await session.assert_that( Expect.path.efficiency( expected_tool_sequence=golden_path, tool_usage_limits={ "validate_input": 1, "process_data": 1, "format_output": 1 }, allow_extra_steps=0, penalize_backtracking=True ), name="golden_path_check" ) ``` When tests fail, examine the actual path: ```python # In your test metrics = session.get_metrics() actual_sequence = [call.name for call in metrics.tool_calls] print(f"Expected: {golden_path}") print(f"Actual: {actual_sequence}") ``` If the agent deviates, improve its instructions: ```python agent = Agent( instruction=""" IMPORTANT: Follow this exact sequence: 1. First validate the input 2. Then process the validated data 3. Finally format the output Never skip steps or backtrack. """ ) ``` Golden paths work best for deterministic workflows. For creative tasks, consider using `allow_extra_steps` or checking only critical waypoints. ## Build quality gates with LLM judges Define what "good" looks like: ```python judge = Expect.judge.llm( rubric=""" The response should: - Accurately summarize the main points - Use clear, professional language - Be 2-3 sentences long """, min_score=0.8, include_input=True # Give judge full context ) ``` Don't rely solely on judges: ```python # Structural check (deterministic) await session.assert_that( Expect.tools.output_matches( tool_name="fetch", expected_output="Example Domain", match_type="contains" ) ) # Quality check (LLM judge) await session.assert_that(judge, response=response) ``` ```python from mcp_eval.evaluators import EvaluationCriterion criteria = [ EvaluationCriterion( name="accuracy", description="All facts are correct and up-to-date", weight=3.0, # Most important min_score=0.9 ), EvaluationCriterion( name="completeness", description="Covers all requested information", weight=2.0, min_score=0.8 ), EvaluationCriterion( name="clarity", description="Easy to understand, well-organized", weight=1.0, min_score=0.7 ) ] judge = Expect.judge.multi_criteria( criteria=criteria, aggregate_method="weighted", # or "min" for strictest require_all_pass=False, # Set True for strict gating use_cot=True # Chain-of-thought reasoning ) ``` Run tests, collect scores, adjust: ```python # Start lenient min_score=0.6 # After collecting data, tighten to p50 or p75 min_score=0.8 # Based on historical performance ``` **Pro tip**: Use Anthropic Claude (Opus or Sonnet) for best judge quality. They provide more consistent and nuanced evaluations. ## Integrate with CI/CD Create `.github/workflows/mcp-eval.yml` using the reusable workflow: ```yaml name: MCP-Eval CI on: push: branches: [main, master, trunk] pull_request: workflow_dispatch: # Cancel redundant runs on the same ref concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: call-mcpeval: uses: lastmile-ai/mcp-eval/.github/workflows/mcpeval-reusable.yml with: deploy-pages: true # Optional: customize test configuration # python-version: '3.11' # tests: 'tests/' # run-args: '-v --max-concurrency 4' permissions: contents: read pages: write id-token: write pull-requests: write secrets: inherit ``` This reusable workflow automatically: * Runs tests and generates reports * Posts PR comments with results * Uploads artifacts * Deploys badges and HTML reports to GitHub Pages (on main branch) In your repository settings: 1. Go to Settings → Pages 2. Source: Deploy from a branch 3. Branch: gh-pages (created automatically by the workflow) 4. Save the settings Your badges and reports will be available at: * Badges: `https://YOUR_USERNAME.github.io/YOUR_REPO/badges/` * Report: `https://YOUR_USERNAME.github.io/YOUR_REPO/` After deploying to GitHub Pages, you may add badges to your README.md to show users your mcp-eval test and coverage status: ```markdown [![mcp-tests](https://img.shields.io/endpoint?url=https://YOUR_USERNAME.github.io/YOUR_REPO/badges/mcp-tests.json&cacheSeconds=300)](https://YOUR_USERNAME.github.io/YOUR_REPO/) [![mcp-cov](https://img.shields.io/endpoint?url=https://YOUR_USERNAME.github.io/YOUR_REPO/badges/mcp-cov.json&cacheSeconds=300)](https://YOUR_USERNAME.github.io/YOUR_REPO/) ``` These badges will automatically update after each push to main. Make tests fail the build appropriately: ```python # In your test critical_assertions = [ Expect.tools.success_rate(min_rate=0.95), Expect.performance.response_time_under(10000) ] for assertion in critical_assertions: result = await session.assert_that(assertion) if not result.passed: sys.exit(1) # Fail CI ``` ## Generate tests with AI Let AI create test scenarios: ```bash # Generate 10 pytest-style tests mcp-eval generate \ --style pytest \ --n-examples 10 \ --provider anthropic \ --model claude-3-5-sonnet-20241022 ``` AI-generated tests are a starting point: ```python # Generated test @task("Test weather fetching") async def test_weather(agent, session): response = await agent.generate_str("Get weather for NYC") await session.assert_that(Expect.tools.was_called("weather_api")) # Add your domain knowledge @task("Test weather fetching with validation") async def test_weather_enhanced(agent, session): response = await agent.generate_str("Get weather for NYC") await session.assert_that(Expect.tools.was_called("weather_api")) # Add specific checks await session.assert_that(Expect.content.regex(r"\d+°[CF]")) await session.assert_that(Expect.content.contains("New York")) ``` Add new scenarios to existing files: ```bash mcp-eval generate \ --update tests/test_weather.py \ --style pytest \ --n-examples 5 ``` ## Debug failing tests ```bash mcp-eval run test_file.py -v ``` Shows tool calls, responses, and assertion details. Look in `test-reports/test_name_*/trace.jsonl`: ```python import json with open("test-reports/test_abc123/trace.jsonl") as f: for line in f: span = json.loads(line) if span["name"].startswith("tool:"): print(f"Tool: {span['name']}") print(f"Duration: {span['duration_ms']}ms") print(f"Input: {span['attributes'].get('input')}") print(f"Output: {span['attributes'].get('output')}") ``` ```bash # Check system health mcp-eval doctor --full # Validate configuration mcp-eval validate ``` ```python # Temporarily add debug output metrics = session.get_metrics() print(f"Tool calls: {[c.name for c in metrics.tool_calls]}") print(f"Total duration: {metrics.total_duration_ms}ms") print(f"Token cost: ${metrics.total_cost_usd}") ``` ## Next steps Master these advanced topics: Systematic evaluation at scale Build domain-specific checks Optimize test execution # Core Concepts Source: https://mcp-eval.ai/concepts Understand the fundamental concepts and architecture of mcp-eval for effective testing. > Master these core concepts to write effective tests with mcp-eval. Each concept builds on the previous ones to create a complete testing framework. ## Overview mcp-eval orchestrates interactions between three key components: 1. **Agents** - AI models that can use tools 2. **MCP Servers** - Tool providers implementing the Model Context Protocol 3. **Test Sessions** - Orchestrators that manage execution and collect metrics Think of it like a stage play: The **agent** is the actor, **MCP servers** provide the props and scenery, and the **test session** is the director capturing everything for review. ## TestSession (single source of truth) `TestSession` is the orchestrator that manages the entire test lifecycle. It configures OpenTelemetry tracing, runs the agent, collects spans, computes metrics, and saves artifacts. ### Key responsibilities * **Trace management**: Configures and captures OTEL traces * **Metrics extraction**: Converts traces into actionable metrics (tool calls, latency, token usage, costs) * **Assertion coordination**: Manages immediate and deferred assertion evaluation * **Report generation**: Creates JSON, HTML, and Markdown reports ### Metrics derived from traces From the OTEL traces, TestSession extracts: * Tool invocation details (names, arguments, outputs, timing) * Iteration counts and conversation turns * Token usage and estimated costs * Performance breakdowns (LLM time vs tool time) * Error patterns and recovery sequences Source: [TestSession](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/session.py) ## TestAgent `TestAgent` is a wrapper around the runtime agent that provides testing-specific functionality and connects assertions to the session. ### Key features * **Simplified API**: `generate_str()` for string responses * **Direct assertion access**: `agent.assert_that()` shortcut * **Session integration**: Automatically connected to TestSession's metrics Example usage: ```python async with test_session("my-test") as session: agent = session.agent response = await agent.generate_str("Do something") await agent.assert_that(Expect.content.contains("done"), response=response) ``` Source: [TestAgent](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/session.py) ## Unified assertion API mcp-eval uses a single, discoverable API pattern for all assertions: ```python await session.assert_that(Expect.category.check(...), response=?, when=?) ``` ### Immediate vs deferred assertions Understanding assertion timing is crucial for debugging test failures. **Immediate assertions** (run when called with a response): * Content checks (`contains`, `regex`) * LLM judges (quality evaluation) **Deferred assertions** (run at session end, need complete metrics): * Tool usage (`was_called`, `count`, `sequence`) * Performance (`response_time_under`, `max_iterations`) * Path efficiency analysis Catalog source: [Expect catalog](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/catalog.py) ## Test styles mcp-eval supports three testing approaches to fit different workflows: ### Decorator style Simple and expressive for quick tests: ```python @task("Verify fetch works") async def test_fetch(agent, session): response = await agent.generate_str("Fetch https://example.com") await session.assert_that(Expect.content.contains("Example")) ``` Features: `@task`, `@setup`, `@teardown`, `@parametrize` ### Pytest integration Familiar for teams already using pytest: ```python @pytest.mark.asyncio async def test_with_pytest(mcp_agent): response = await mcp_agent.generate_str("Fetch data") assert "success" in response ``` Features: fixtures (`mcp_session`, `mcp_agent`), markers (`@pytest.mark.mcp_agent`) ### Dataset style Systematic evaluation with test matrices: ```python dataset = Dataset( name="Comprehensive suite", cases=[ Case("test_1", inputs="Do X", evaluators=[...]), Case("test_2", inputs="Do Y", evaluators=[...]) ] ) ``` Sources: * [Decorator core](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/core.py) * [Pytest plugin](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/pytest_plugin.py) * [Dataset API](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/datasets.py) ## LLM judges LLM-based evaluation for subjective quality assessment: ### Single criterion ```python judge = Expect.judge.llm( rubric="Response should be helpful and accurate", min_score=0.8 ) ``` ### Multi-criteria evaluation ```python criteria = [ EvaluationCriterion("accuracy", "Factually correct", weight=2.0), EvaluationCriterion("clarity", "Easy to understand", weight=1.5) ] judge = Expect.judge.multi_criteria(criteria) ``` Configuration via `MCPEvalSettings` for judge model/provider defaults. Sources: * [LLMJudge](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/evaluators/llm_judge.py) * [MultiCriteriaJudge](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/evaluators/multi_criteria_judge.py) ## Architecture flow ```mermaid sequenceDiagram participant Test participant Session participant Agent participant MCP Server participant OTEL Test->>Session: Initialize Session->>OTEL: Configure tracing Test->>Agent: Send prompt Agent->>MCP Server: Tool request MCP Server->>Agent: Tool response Agent->>OTEL: Emit spans Agent->>Test: Return response Test->>Session: Assert Session->>OTEL: Process traces Session->>Test: Results ``` ## Next steps With these concepts understood: Start with a simple example Learn the full assertion API # Configuration Guide Source: https://mcp-eval.ai/configuration Master mcp-eval configuration. Learn file structures, precedence rules, environment variables, and programmatic APIs for complete control over your testing environment. > ⚙️ **Configure with confidence!** This comprehensive guide covers every configuration option, from basic setup to advanced customization. You'll learn exactly how to tune mcp-eval for your specific needs. ## Quick configuration finder What do you need to configure? Essential settings to get started MCP server connections Agent behavior and models LLM providers and API keys Test execution settings Output formats and locations ## Configuration overview mcp-eval uses a layered configuration system that gives you flexibility and control: {/* TODO: Diagram showing configuration file hierarchy and precedence */} ### File precedence (later overrides earlier) 1. **`mcp-agent.config.yaml`** - Base configuration for servers and providers 2. **`mcp-agent.secrets.yaml`** - Secure API keys and credentials 3. **`mcpeval.yaml`** - mcp-eval specific settings 4. **`mcpeval.secrets.yaml`** - mcp-eval specific secrets 5. **Environment variables** - Runtime overrides 6. **Programmatic configuration** - Code-level settings ### File discovery mcp-eval searches for configuration files in this order: ``` Current directory: ├── mcpeval.yaml ├── mcpeval.secrets.yaml ├── mcp-agent.config.yaml ├── mcp-agent.secrets.yaml └── .mcp-eval/ ├── config.yaml └── secrets.yaml Parent directories (recursive): └── (same structure) Home directory: └── ~/.mcp-eval/ ├── config.yaml └── secrets.yaml ``` ## Basic configuration Let's start with a complete, working configuration: ### Complete mcpeval.yaml example ```yaml # mcpeval.yaml $schema: ./schema/mcpeval.config.schema.json # Metadata name: "My MCP Test Suite" description: "Comprehensive testing for our MCP servers" # Default LLM provider settings provider: "anthropic" model: "claude-3-5-sonnet-20241022" # Default agent for tests default_agent: name: "test_agent" instruction: "You are a helpful testing assistant. Be precise and thorough." server_names: ["calculator", "weather"] # Judge configuration judge: provider: "anthropic" # Can differ from main provider model: "claude-3-5-sonnet-20241022" min_score: 0.8 max_tokens: 1000 system_prompt: "You are an expert evaluator. Be fair but strict." # Metrics collection metrics: collect: - "response_time" - "tool_coverage" - "iteration_count" - "token_usage" - "cost_estimate" - "error_rate" - "path_efficiency" # Reporting configuration reporting: formats: ["json", "markdown", "html"] output_dir: "./test-reports" include_traces: true include_config: true timestamp_format: "%Y%m%d_%H%M%S" # Test execution settings execution: max_concurrency: 5 timeout_seconds: 300 retry_failed: true retry_count: 3 retry_delay: 5 parallel: true stop_on_first_failure: false verbose: false debug: false # Logging configuration logging: level: "INFO" # DEBUG, INFO, WARNING, ERROR format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" file: "test-reports/mcp-eval.log" console: true show_mcp_messages: false # Set true for debugging # Cache configuration cache: enabled: true ttl: 3600 # 1 hour directory: ".mcp-eval-cache" # Development settings development: mock_llm_responses: false save_llm_calls: true profile_performance: false ``` ### Minimal configuration If you just want to get started quickly: ```yaml # mcpeval.yaml (minimal) provider: "anthropic" model: "claude-3-haiku-20240307" mcp: servers: my_server: command: "python" args: ["server.py"] ``` ## Server configuration Configure your MCP servers for testing: ### Basic server setup ```yaml # In mcp-agent.config.yaml or mcpeval.yaml mcp: servers: # Simple Python server calculator: command: "python" args: ["servers/calculator.py"] env: LOG_LEVEL: "DEBUG" # Node.js server with npm weather: command: "npm" args: ["run", "start:weather"] cwd: "./servers/weather" # Pre-built server from package fetch: command: "uvx" args: ["mcp-server-fetch"] env: UV_NO_PROGRESS: "1" # Docker container server database: command: "docker" args: ["run", "--rm", "-i", "my-mcp-server:latest"] startup_timeout: 30 # Wait for container to start ``` ### Advanced server options ```yaml mcp: servers: advanced_server: # Transport configuration transport: "stdio" # or "http" for HTTP transport # For HTTP transport url: "http://localhost:8080" headers: Authorization: "Bearer ${SERVER_API_KEY}" # Command execution command: "python" args: ["server.py", "--port", "8080"] cwd: "/path/to/server" # Environment variables env: DATABASE_URL: "${DATABASE_URL}" API_KEY: "${API_KEY}" DEBUG: "true" # Lifecycle management startup_timeout: 10 # Seconds to wait for startup shutdown_timeout: 5 # Seconds to wait for shutdown restart_on_failure: true max_restarts: 3 # Health checks health_check: endpoint: "/health" interval: 30 timeout: 5 # Resource limits resources: max_memory: "512M" max_cpu: "1.0" ``` ### Importing servers from other sources ```yaml # Import from mcp.json (Cursor/VS Code) mcp: import: - type: "mcp_json" path: ".cursor/mcp.json" # Import from DXT manifest - type: "dxt" path: "~/Desktop/my-manifest.dxt" ``` ## Agent configuration Define agents for different testing scenarios: ### Agent specifications ```yaml # In mcp-agent.config.yaml agents: - name: "comprehensive_tester" instruction: | You are a thorough testing agent. Your job is to: 1. Test all available tools systematically 2. Verify outputs are correct 3. Handle errors gracefully 4. Report issues clearly server_names: ["calculator", "weather", "database"] model: "claude-3-5-sonnet-20241022" temperature: 0 # Deterministic for testing max_tokens: 4000 - name: "minimal_tester" instruction: "Test basic functionality quickly." server_names: ["calculator"] model: "claude-3-haiku-20240307" # Cheaper for simple tests # Subagents for specific tasks subagents: enabled: true search_paths: - ".claude/agents" - ".mcp-agent/agents" pattern: "**/*.yaml" inline: - name: "error_specialist" instruction: "Focus on finding and testing error conditions." server_names: ["*"] # Access to all servers functions: - name: "validate_error" description: "Check if error is handled correctly" ``` ### Agent selection strategies ```yaml # Use specific agent for different test types test_strategies: unit: agent: "minimal_tester" timeout: 60 integration: agent: "comprehensive_tester" timeout: 300 stress: agent: "stress_tester" timeout: 600 max_iterations: 100 ``` ## Provider configuration Configure LLM providers and authentication: ### Anthropic configuration ```yaml # In mcpeval.secrets.yaml (keep out of version control!) anthropic: api_key: "sk-ant-api03-..." base_url: "https://api.anthropic.com" # Optional custom endpoint default_model: "claude-3-5-sonnet-20241022" # Model-specific settings models: claude-3-5-sonnet-20241022: max_tokens: 8192 temperature: 0.7 top_p: 0.95 claude-3-haiku-20240307: max_tokens: 4096 temperature: 0.3 # More deterministic for testing ``` ### OpenAI configuration ```yaml # In mcpeval.secrets.yaml openai: api_key: "sk-..." organization: "org-..." # Optional base_url: "https://api.openai.com/v1" default_model: "gpt-4-turbo-preview" models: gpt-4-turbo-preview: max_tokens: 4096 temperature: 0.5 presence_penalty: 0.1 frequency_penalty: 0.1 ``` ### Environment variable overrides ```bash # Override configuration via environment export ANTHROPIC_API_KEY="sk-ant-..." export OPENAI_API_KEY="sk-..." # Custom provider settings export MCP_EVAL_PROVIDER="anthropic" export MCP_EVAL_MODEL="claude-3-5-sonnet-20241022" export MCP_EVAL_TIMEOUT="600" ``` ## Test execution configuration Fine-tune how tests are executed: ### Execution strategies ```yaml execution: # Concurrency control max_concurrency: 5 # Max parallel tests max_workers: 10 # Max parallel tool calls # Timeout management timeout_seconds: 300 # Global timeout timeouts: unit: 60 integration: 300 stress: 600 # Retry logic retry_failed: true retry_count: 3 retry_delay: 5 # Seconds between retries retry_backoff: "exponential" # or "linear" retry_on_errors: - "RateLimitError" - "NetworkError" - "TimeoutError" # Execution control parallel: true randomize_order: false # Run tests in random order stop_on_first_failure: false fail_fast_threshold: 0.5 # Stop if >50% fail # Resource management max_memory_mb: 2048 kill_timeout: 10 # Force kill after this many seconds # Test selection markers: skip: ["slow", "flaky"] # Skip these markers only: [] # Only run these markers patterns: include: ["test_*.py", "*_test.py"] exclude: ["test_experimental_*.py"] ``` ### Performance optimization ```yaml performance: # Caching cache_llm_responses: true cache_ttl: 3600 cache_size_mb: 100 # Batching batch_size: 10 # Process tests in batches batch_timeout: 30 # Rate limiting requests_per_second: 10 burst_limit: 20 # Connection pooling max_connections: 20 connection_timeout: 10 # Memory management gc_threshold: 100 # Force garbage collection after N tests clear_cache_after: 50 # Clear caches after N tests ``` ## Reporting configuration Control how results are reported: ### Output formats and locations ```yaml reporting: # Output formats formats: - "json" # Machine-readable - "markdown" # Human-readable - "html" # Interactive - "junit" # CI integration - "csv" # Spreadsheet analysis # Output configuration output_dir: "./test-reports" create_subdirs: true # Organize by date/time # Report naming filename_template: "{suite}_{timestamp}_{status}" timestamp_format: "%Y%m%d_%H%M%S" # Content options include_traces: true include_config: true include_environment: true include_git_info: true include_system_info: true # Report detail levels verbosity: console: "summary" # minimal, summary, detailed, verbose file: "detailed" html: "verbose" # Filtering show_passed: true show_failed: true show_skipped: false max_output_length: 10000 # Truncate long outputs # Metrics and analytics calculate_statistics: true generate_charts: true trend_analysis: true # Notifications notifications: slack: webhook_url: "${SLACK_WEBHOOK}" on_failure: true on_success: false email: smtp_server: "smtp.gmail.com" from: "tests@example.com" to: ["team@example.com"] on_failure: true ``` ### Custom report templates ```yaml reporting: templates: markdown: "templates/custom_report.md.jinja" html: "templates/custom_report.html.jinja" custom_fields: project_name: "My MCP Project" team: "Platform Team" environment: "staging" ``` ## Judge configuration Configure LLM judges for quality evaluation: ```yaml judge: # Provider settings (can differ from main provider) provider: "anthropic" model: "claude-3-5-sonnet-20241022" # Scoring configuration min_score: 0.8 # Global minimum score score_thresholds: critical: 0.95 high: 0.85 medium: 0.70 low: 0.50 # Judge behavior max_tokens: 2000 temperature: 0.3 # Lower for consistency # Judge prompts system_prompt: | You are an expert quality evaluator for AI responses. Be thorough, fair, and consistent in your evaluations. Provide clear reasoning for your scores. # Evaluation settings require_reasoning: true require_confidence: true use_cot: true # Chain-of-thought # Multi-criteria defaults multi_criteria: aggregate_method: "weighted" # weighted, min, harmonic_mean require_all_pass: false min_criteria_score: 0.7 # Calibration calibration: enabled: true samples: 100 adjust_thresholds: true ``` ## Environment-specific configuration Different settings for different environments: ### Development configuration ```yaml # mcpeval.dev.yaml $extends: "./mcpeval.yaml" # Inherit base config provider: "anthropic" model: "claude-3-haiku-20240307" # Cheaper for dev execution: max_concurrency: 1 # Easier debugging timeout_seconds: 600 # More time for debugging debug: true development: mock_llm_responses: true # Use mocked responses save_llm_calls: true profile_performance: true logging: level: "DEBUG" show_mcp_messages: true ``` ### CI/CD configuration ```yaml # mcpeval.ci.yaml $extends: "./mcpeval.yaml" execution: max_concurrency: 10 # Maximize parallelism timeout_seconds: 180 # Strict timeouts retry_failed: false # Don't hide flaky tests stop_on_first_failure: true reporting: formats: ["junit", "json"] # CI-friendly formats ci: fail_on_quality_gate: true min_pass_rate: 0.95 max_test_duration: 300 ``` ### Production configuration ```yaml # mcpeval.prod.yaml $extends: "./mcpeval.yaml" provider: "anthropic" model: "claude-3-5-sonnet-20241022" # Best model for production execution: max_concurrency: 20 timeout_seconds: 120 retry_failed: true retry_count: 5 monitoring: enabled: true metrics_endpoint: "https://metrics.example.com" alerting: enabled: true thresholds: error_rate: 0.05 p95_latency: 5000 ``` ## Programmatic configuration Configure mcp-eval from code: ### Basic programmatic setup ```python from mcp_eval.config import set_settings, MCPEvalSettings, use_agent from mcp_agent.agents.agent import Agent # Configure via dictionary set_settings({ "provider": "anthropic", "model": "claude-3-5-sonnet-20241022", "reporting": { "output_dir": "./my-reports", "formats": ["html", "json"] }, "execution": { "timeout_seconds": 120, "max_concurrency": 3 } }) # Or use typed settings settings = MCPEvalSettings( provider="anthropic", model="claude-3-haiku-20240307", judge={"min_score": 0.85}, reporting={"output_dir": "./test-output"} ) set_settings(settings) # Configure agent agent = Agent( name="my_test_agent", instruction="Test thoroughly", server_names=["my_server"] ) use_agent(agent) ``` ### Advanced programmatic control ```python from mcp_eval.config import ( load_config, get_settings, use_config, ProgrammaticDefaults ) # Load specific config file config = load_config("configs/staging.yaml") use_config(config) # Modify settings at runtime current = get_settings() current.execution.timeout_seconds = 600 current.reporting.formats.append("csv") # Set programmatic defaults defaults = ProgrammaticDefaults() defaults.set_agent_factory(lambda: create_custom_agent()) defaults.set_default_servers(["server1", "server2"]) # Context manager for temporary config from mcp_eval.config import config_context with config_context({"provider": "openai", "model": "gpt-4"}): # Tests here use OpenAI run_tests() # Back to original config ``` ### Environment variable reference Complete list of environment variables: ```bash # Provider settings ANTHROPIC_API_KEY="sk-ant-..." OPENAI_API_KEY="sk-..." MCP_EVAL_PROVIDER="anthropic" MCP_EVAL_MODEL="claude-3-5-sonnet-20241022" # Execution settings MCP_EVAL_TIMEOUT="300" MCP_EVAL_MAX_CONCURRENCY="5" MCP_EVAL_RETRY_COUNT="3" MCP_EVAL_DEBUG="true" # Reporting MCP_EVAL_OUTPUT_DIR="./reports" MCP_EVAL_REPORT_FORMATS="json,html,markdown" # Judge settings MCP_EVAL_JUDGE_MODEL="claude-3-5-sonnet-20241022" MCP_EVAL_JUDGE_MIN_SCORE="0.8" # Development MCP_EVAL_MOCK_LLM="false" MCP_EVAL_SAVE_TRACES="true" MCP_EVAL_PROFILE="false" # Logging MCP_EVAL_LOG_LEVEL="INFO" MCP_EVAL_LOG_FILE="mcp-eval.log" ``` ## Configuration validation Ensure your configuration is correct: ### Using the validate command ```bash # Validate all configuration mcp-eval validate # Validate specific aspects mcp-eval validate --servers mcp-eval validate --agents ``` ### Programmatic validation ```python from mcp_eval.config import validate_config # Validate configuration errors = validate_config("mcpeval.yaml") if errors: print("Configuration errors:") for error in errors: print(f" - {error}") sys.exit(1) ``` ### Schema validation ```yaml # Add schema reference for IDE support $schema: "./schema/mcpeval.config.schema.json" # Your configuration here... ``` ## Best practices Follow these guidelines for maintainable configuration: Never commit API keys. Use `.secrets.yaml` files and add to `.gitignore` Create dev, staging, and prod configs that extend a base configuration Add comments explaining non-obvious configuration choices Run `mcp-eval validate` in CI to catch configuration issues early Track configuration changes except for secrets files Set sensible defaults but allow overrides for flexibility ## Troubleshooting configuration Common configuration issues and solutions: | Issue | Solution | | ------------------ | --------------------------------------------------- | | Config not found | Check file name and location, use `--config` flag | | Invalid YAML | Validate syntax with `yamllint` or online validator | | Server won't start | Check command path, permissions, and dependencies | | API key errors | Verify key in secrets file or environment variable | | Wrong model used | Check precedence: code > env > config file | | Timeout too short | Increase `execution.timeout_seconds` | {/* TODO: Screenshot of mcp-eval validate output showing successful configuration */} ## Configuration examples ### Minimal testing setup ```yaml # Quick start configuration provider: "anthropic" model: "claude-3-haiku-20240307" mcp: servers: my_server: command: "python" args: ["server.py"] ``` ### Comprehensive testing suite See the complete example at the beginning of this guide. ### Multi-environment setup ```bash # Directory structure configs/ ├── base.yaml # Shared configuration ├── dev.yaml # Development overrides ├── staging.yaml # Staging overrides ├── prod.yaml # Production settings └── secrets.yaml # API keys (gitignored) ``` *** **You're now a configuration expert!** With this knowledge, you can tune mcp-eval to work perfectly for your specific testing needs. Remember: start simple and add complexity as needed! 🎯 # Connect MCP Servers Source: https://mcp-eval.ai/connect-servers Configure and validate MCP servers via mcp.json, DXT manifests, or programmatic settings. > Connect your Model Context Protocol (MCP) servers so mcp‑eval can exercise real tools during evaluation. ## Options at a glance * Add from `mcp.json` (project‑scoped) * Import a DXT manifest * Programmatic defaults in config or per Agent * Mix local stdio with remote SSE/HTTP servers ## Add servers from mcp.json If your project already defines servers in a `mcp.json` file (or a path you specify), you can import them: ```bash mcp-eval server add --from-mcp-json .cursor/mcp.json ``` Reference: `mcp.json` format and conventions at `https://gofastmcp.com/integrations/mcp-json-configuration`. ## Add servers from a DXT manifest DXT manifests describe an MCP server (command/args/env or URL). Import directly: ```bash mcp-eval server add --from-dxt ~/Desktop/manifest.dxt ``` Reference: DXT documentation at `https://github.com/anthropics/dxt/blob/main/README.md`. ## Programmatic configuration (global defaults) You can set default servers for all tests via the evaluation config. In `mcpeval.yaml`: ```yaml default_servers: - sse:https://mcp.example.com/sse - http:https://mcp.example.com/mcp - stdio:npx -y @my/mcp-server ``` Or programmatically: ```python from mcp_eval.config import update_config update_config({ "default_servers": [ "sse:https://mcp.example.com/sse", "stdio:npx -y @my/mcp-server", ] }) ``` ## Per‑Agent servers If you are constructing an `Agent` or `AgentSpec`, you can override servers at the agent level (recommended for multi‑server agents): ```python from mcp_agent.core import AgentSpec spec = AgentSpec( name="fetch-agent", servers=[ "sse:https://mcp.fetcher.dev/sse", "stdio:npx -y @bytebase/dbhub", ], ) ``` ## Validate connectivity Before running tests, verify configuration and connectivity: ```bash # Quick config checks (no network) mcp-eval validate --quick # Full validation: connect servers, spin agent, basic probes mcp-eval validate # List discovered servers and available tools mcp-eval server list -v | cat ``` ### Tips * Prefer SSE/HTTP for hosted SaaS servers; use stdio for local CLIs. * Keep secrets in your secrets file/env rather than hardcoding in `mcp.json`/DXT. * For multi‑server agents, name servers clearly and use `Expect.tools.*` assertions to verify the correct server/tool was used. # Datasets Source: https://mcp-eval.ai/datasets Define Cases and Datasets for systematic evaluation; run programmatically or from files. ### API * `Case[Input, Output, Metadata]` * `Dataset[Input, Output, Metadata]` Source: [datasets.py](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/datasets.py) ### Programmatic ```python from mcp_eval import Case, Dataset, ToolWasCalled, ResponseContains cases = [ Case( name="fetch_example", inputs="Fetch https://example.com", evaluators=[ToolWasCalled("fetch"), ResponseContains("Example Domain")], ) ] dataset = Dataset(name="Fetch Suite", cases=cases) report = await dataset.evaluate(lambda inputs, agent, session: agent.generate_str(inputs)) report.print(include_input=True, include_output=True) ``` Parallel evaluation: ```python report = await dataset.evaluate( lambda inputs, agent, session: agent.generate_str(inputs), max_concurrency=4, ) ``` ### YAML/JSON Save/load via `Dataset.to_file` and `Dataset.from_file`. Schema: [mcpeval.config.schema.json](https://github.com/lastmile-ai/mcp-eval/blob/main/schema/mcpeval.config.schema.json). YAML example (from [basic\_fetch\_dataset.yaml](https://github.com/lastmile-ai/mcp-eval/blob/main/examples/mcp_server_fetch/datasets/basic_fetch_dataset.yaml)): ```yaml name: "Basic Fetch Dataset" server_name: "fetch" cases: - name: "simple_fetch" inputs: "Fetch https://example.com" expected_output: "Example Domain" evaluators: - ToolWasCalled: tool_name: "fetch" - ResponseContains: text: "Example Domain" ``` ### Concurrency `Dataset.evaluate(..., max_concurrency=N)` runs cases in parallel. ### Examples * [test\_dataset\_style.py](https://github.com/lastmile-ai/mcp-eval/blob/main/examples/mcp_server_fetch/tests/test_dataset_style.py) * [datasets/](https://github.com/lastmile-ai/mcp-eval/tree/main/examples/mcp_server_fetch/datasets/) {/* TODO: Add a screenshot showing dataset progress UI and a snippet of the printed table. */} # Detailed Guide Source: https://mcp-eval.ai/detailed_guide A comprehensive, single-page guide to `mcp-eval`: concepts, setup, styles, assertions, metrics, CLI, and best practices. This is a long-form guide adapted from and expanding on [GUIDE.md](https://github.com/lastmile-ai/mcp-eval/blob/main/GUIDE.md). It’s organized for readers who prefer a single page. ## What is `mcp-eval`? Think of `mcp-eval` as your “flight simulator” for tool‑using LLMs. You plug in an agent, connect it to real MCP servers (tools), and run realistic scenarios. The framework captures OTEL traces as the single source of truth, turns them into metrics, and gives you expressive assertions for both content and behavior. ### Core pieces * [TestSession and TestAgent](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/session.py) * [Decorators and task runner](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/core.py) * [Dataset and Case](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/datasets.py) * [Expect (assertion catalog)](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/catalog.py) * [Evaluators](https://github.com/lastmile-ai/mcp-eval/tree/main/src/mcp_eval/evaluators/) and [Metrics](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/metrics.py) * [Runner](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/runner.py) and [CLI](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/cli/__init__.py) ## Getting Started 1. **Install mcp-eval globally**: `uv tool install mcpevals` (recommended) or `pip install mcpevals` 2. **Initialize your project**: `mcp-eval init` - interactive setup for API keys and configuration 3. **Add your MCP server**: `mcp-eval server add` - configure the server you want to test 4. **Run tests**: `mcp-eval run tests/` - execute your test suite **Test servers written in any language:** Your MCP server can be written in Python, TypeScript, Go, Rust, Java, or any other language. `mcp-eval` connects to it via the MCP protocol, making testing completely language-agnostic. See the [Quickstart](./quickstart) page for detailed setup instructions. {/* TODO: Insert screenshots of init prompts and the first run summary. */} ## Styles of Tests ### Decorator style ```python from mcp_eval import Expect from mcp_eval import task, setup, teardown, parametrize @task("Test basic URL fetching functionality") async def test_basic_fetch(agent, session): response = await agent.generate_str("Fetch the content from https://example.com") await session.assert_that(Expect.tools.was_called("fetch"), name="fetch_called", response=response) await session.assert_that(Expect.content.contains("Example Domain"), name="contains_domain", response=response) ``` Full example: [test\_decorator\_style.py](https://github.com/lastmile-ai/mcp-eval/blob/main/examples/mcp_server_fetch/tests/test_decorator_style.py) ### Pytest style ```python import pytest from mcp_eval import Expect @pytest.mark.asyncio async def test_basic_fetch_with_pytest(mcp_agent): response = await mcp_agent.generate_str("Fetch the content from https://example.com") await mcp_agent.session.assert_that(Expect.tools.was_called("fetch"), name="fetch_called", response=response) await mcp_agent.session.assert_that(Expect.content.contains("Example Domain"), name="contains_text", response=response) ``` Full example: [test\_pytest\_style.py](https://github.com/lastmile-ai/mcp-eval/blob/main/examples/mcp_server_fetch/tests/test_pytest_style.py) ### Dataset style ```python from mcp_eval import Case, Dataset, ToolWasCalled, ResponseContains cases = [ Case(name="fetch_example", inputs="Fetch https://example.com", evaluators=[ToolWasCalled("fetch"), ResponseContains("Example Domain")]) ] dataset = Dataset(name="Fetch Suite", cases=cases) report = await dataset.evaluate(lambda inputs, agent, session: agent.generate_str(inputs)) report.print(include_input=True, include_output=True) ``` Full example: [test\_dataset\_style.py](https://github.com/lastmile-ai/mcp-eval/blob/main/examples/mcp_server_fetch/tests/test_dataset_style.py) and [basic\_fetch\_dataset.yaml](https://github.com/lastmile-ai/mcp-eval/blob/main/examples/mcp_server_fetch/datasets/basic_fetch_dataset.yaml) ## Assertions and Timing Immediate vs deferred execution of evaluators is handled automatically based on whether final metrics are required. See [Assertions](./assertions). ## Agent Evaluation Define your agent as the system under test via [use\_agent and with\_agent](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/config.py). See [Agent Evaluation](./agent-evaluation) for patterns and metrics to watch. ## Server Evaluation Connect an MCP server, then write scenarios that exercise it through an agent. Use tool/path/efficiency assertions. See [Server Evaluation](./server-evaluation). ## Metrics & Tracing OTEL is the source of truth. After a run, explore metrics and the span tree for loops, path inefficiency, and recovery. See [Metrics & Tracing](./metrics-tracing). ## Test Generation with LLMs Use `mcp-eval generate` to bootstrap comprehensive tests. We recommend Anthropic Sonnet/Opus. See [Test Generation](./test-generation). ## CI/CD Run in GitHub Actions and publish artifacts/badges. See [CI/CD](./ci-cd). ## Troubleshooting Use `mcp-eval doctor`, `validate`, and `issue` for diagnosis. See [Troubleshooting](./troubleshooting). ## Best Practices * Prefer objective, structural checks alongside LLM judges * Keep prompts clear and deterministic; gate performance separately (nightly) * Use parametrization to widen coverage * Keep servers in mcp‑agent config; use `mcpeval.yaml` for eval knobs {/* TODO: Add a closing diagram summarizing the `mcp-eval` lifecycle (init → write tests → run → metrics → reports). */} # Complete Examples Source: https://mcp-eval.ai/examples Learn by example! Complete, runnable test suites for MCP servers and agents, covering all testing patterns and real-world scenarios. > 🎯 **Learn by doing!** These complete examples show you exactly how to test MCP servers and agents in real-world scenarios. Copy, paste, and adapt them for your needs! ## Quick example finder What are you trying to test? Simple tool verification and output checking Comprehensive agent behavior testing Robust error recovery testing Efficiency and optimization checks Multi-server orchestration tests Systematic test coverage with datasets ## Basic server testing Let's start with a simple but complete example testing a calculator MCP server: {/* TODO: Screenshot of test output showing passed assertions */} ```python """Complete test suite for calculator MCP server using decorator style.""" from mcp_eval import task, setup, teardown, parametrize, Expect from mcp_eval.session import TestAgent, TestSession @setup def configure_calculator_tests(): """Setup before all calculator tests.""" print("🧮 Starting calculator server tests") @teardown def cleanup_calculator_tests(): """Cleanup after all tests.""" print("✅ Calculator tests completed") @task("Test basic addition") async def test_addition(agent: TestAgent, session: TestSession): """Verify the calculator can perform addition correctly.""" response = await agent.generate_str( "Use the calculator to add 15 and 27" ) # Verify tool was called await session.assert_that( Expect.tools.was_called("calculate"), name="calculator_called" ) # Check the calculation arguments await session.assert_that( Expect.tools.called_with( "calculate", {"operation": "add", "a": 15, "b": 27} ), name="correct_arguments" ) # Verify the result await session.assert_that( Expect.content.contains("42"), name="correct_result", response=response ) @parametrize( "operation,a,b,expected", [ ("add", 10, 5, "15"), ("subtract", 10, 3, "7"), ("multiply", 4, 7, "28"), ("divide", 20, 4, "5"), ] ) @task("Test all operations") async def test_operations( agent: TestAgent, session: TestSession, operation: str, a: int, b: int, expected: str ): """Test all calculator operations with various inputs.""" response = await agent.generate_str( f"Use the calculator to {operation} {a} and {b}" ) await session.assert_that( Expect.content.contains(expected), name=f"{operation}_result", response=response ) await session.assert_that( Expect.tools.success_rate(min_rate=1.0, tool_name="calculate"), name=f"{operation}_success" ) @task("Test division by zero handling") async def test_division_by_zero(agent: TestAgent, session: TestSession): """Verify graceful handling of division by zero.""" response = await agent.generate_str( "Try to divide 10 by 0 using the calculator" ) # Tool should be called but may fail await session.assert_that( Expect.tools.was_called("calculate"), name="attempted_division" ) # Response should handle error gracefully await session.assert_that( Expect.judge.llm( rubric="Response acknowledges division by zero error and explains it clearly", min_score=0.8 ), name="error_handling", response=response ) ``` ```python """Complete test suite for calculator MCP server using pytest.""" import pytest from mcp_eval import Expect class TestCalculator: """Calculator server test suite.""" @pytest.mark.asyncio async def test_basic_addition(self, mcp_agent): """Test basic addition operation.""" response = await mcp_agent.generate_str( "Calculate 15 + 27" ) # Verify using session assertions await mcp_agent.session.assert_that( Expect.tools.was_called("calculate"), Expect.content.contains("42"), response=response ) @pytest.mark.asyncio @pytest.mark.parametrize( "a,b,operation,expected", [ (10, 5, "add", "15"), (10, 3, "subtract", "7"), (4, 7, "multiply", "28"), (20, 4, "divide", "5"), ] ) async def test_all_operations( self, mcp_agent, a, b, operation, expected ): """Test all calculator operations.""" prompt_map = { "add": f"Add {a} and {b}", "subtract": f"Subtract {b} from {a}", "multiply": f"Multiply {a} by {b}", "divide": f"Divide {a} by {b}" } response = await mcp_agent.generate_str(prompt_map[operation]) assert expected in response # Also check with Expect API await mcp_agent.session.assert_that( Expect.tools.was_called("calculate"), name=f"{operation}_called" ) @pytest.mark.asyncio async def test_error_handling(self, mcp_agent): """Test error handling for invalid operations.""" response = await mcp_agent.generate_str( "Divide 10 by 0" ) # Should handle error gracefully await mcp_agent.session.assert_that( Expect.judge.llm( "Handles division by zero appropriately", min_score=0.7 ), response=response ) ``` Run with uv: ```bash uv run pytest -q examples/mcp_server_fetch/tests/test_pytest_style.py # Or a single file/function uv run pytest examples/mcp_server_fetch/tests/test_pytest_style.py -v uv run pytest examples/mcp_server_fetch/tests/test_pytest_style.py::TestCalculator::test_basic_addition -v ``` ## Agent evaluation suite Testing an agent's reasoning, tool selection, and response quality: {/* TODO: Screenshot of agent evaluation report showing reasoning paths */} ```python """Comprehensive agent evaluation suite.""" from mcp_eval import task, Expect from mcp_eval.evaluators import EvaluationCriterion @task("Test agent reasoning quality") async def test_agent_reasoning(agent, session): """Evaluate the agent's reasoning capabilities.""" response = await agent.generate_str( "I have 3 apples. I eat one, buy 5 more, then give away 2. " "How many apples do I have? Show your reasoning." ) # Multi-criteria evaluation criteria = [ EvaluationCriterion( name="correct_answer", description="Arrives at the correct answer of 5 apples", weight=3.0, min_score=0.9 ), EvaluationCriterion( name="clear_reasoning", description="Shows clear step-by-step reasoning", weight=2.0, min_score=0.8 ), EvaluationCriterion( name="calculation_accuracy", description="All intermediate calculations are correct", weight=2.0, min_score=0.9 ) ] await session.assert_that( Expect.judge.multi_criteria( criteria=criteria, aggregate_method="weighted", require_all_pass=False ), name="reasoning_quality", response=response ) @task("Test tool selection intelligence") async def test_tool_selection(agent, session): """Verify agent selects appropriate tools for tasks.""" # Agent has access to: calculator, web_search, file_reader response = await agent.generate_str( "First, calculate 15% of 200. Then search for the " "current population of Tokyo. Finally, read the " "contents of config.json" ) # Check tool sequence await session.assert_that( Expect.tools.sequence( ["calculator", "web_search", "file_reader"], allow_other_calls=True ), name="correct_tool_sequence" ) # Verify all tools succeeded await session.assert_that( Expect.tools.success_rate(min_rate=1.0), name="all_tools_succeeded" ) # Check efficiency await session.assert_that( Expect.path.efficiency( expected_tool_sequence=["calculator", "web_search", "file_reader"], allow_extra_steps=0, penalize_repeated_tools=True ), name="efficient_tool_usage" ) @task("Test context retention") async def test_context_retention(agent, session): """Verify agent maintains context across interactions.""" # First interaction response1 = await agent.generate_str( "My name is Alice and I like blue. Remember this." ) # Second interaction referencing first response2 = await agent.generate_str( "What's my name and favorite color?" ) await session.assert_that( Expect.content.contains("Alice", case_sensitive=True), name="remembers_name", response=response2 ) await session.assert_that( Expect.content.contains("blue", case_sensitive=False), name="remembers_color", response=response2 ) ``` ## Error handling patterns Robust testing for error scenarios and recovery: ```python """Error handling and recovery test patterns.""" from mcp_eval import task, Expect import asyncio @task("Test graceful degradation") async def test_graceful_degradation(agent, session): """Verify system degrades gracefully when tools fail.""" response = await agent.generate_str( "Try to fetch https://this-domain-definitely-does-not-exist-12345.com " "If that fails, explain what HTTP status codes mean." ) # Should attempt the fetch await session.assert_that( Expect.tools.was_called("fetch"), name="attempted_fetch" ) # Should handle failure gracefully await session.assert_that( Expect.judge.llm( rubric=""" Response should: 1. Acknowledge the fetch failed 2. Provide useful information about HTTP status codes 3. Not expose internal errors to the user """, min_score=0.8 ), name="graceful_degradation", response=response ) @task("Test retry logic") async def test_retry_behavior(agent, session): """Test agent's retry behavior on transient failures.""" response = await agent.generate_str( "Fetch data from the API endpoint /flaky-endpoint " "(this endpoint fails 50% of the time randomly)" ) # Should retry on failure await session.assert_that( Expect.tools.was_called("fetch", min_times=1), name="fetch_attempted" ) # Eventually should succeed or give up gracefully await session.assert_that( Expect.judge.llm( "Either successfully retrieved data or clearly explained the failure", min_score=0.9 ), name="handled_flaky_endpoint", response=response ) @task("Test timeout handling") async def test_timeout_handling(agent, session): """Verify proper timeout handling.""" # Simulate slow operation response = await agent.generate_str( "Fetch data from /slow-endpoint (takes 10 seconds to respond)", timeout=5 # Set shorter timeout ) # Should handle timeout appropriately await session.assert_that( Expect.judge.llm( "Response indicates timeout or long wait time appropriately", min_score=0.8 ), name="timeout_handled", response=response ) @task("Test input validation") async def test_input_validation(agent, session): """Test handling of invalid inputs.""" test_cases = [ "Calculate the square root of -1", "Divide by the string 'hello'", "Fetch from URL: not-a-valid-url", "Read file: /etc/shadow" # Permission denied ] for test_input in test_cases: response = await agent.generate_str(test_input) # Should handle invalid input gracefully await session.assert_that( Expect.judge.llm( f"Handles invalid input appropriately: {test_input}", min_score=0.7 ), name=f"validates_{test_input[:20]}", response=response ) ``` ## Performance testing Testing efficiency, speed, and resource usage: {/* TODO: Screenshot of performance metrics dashboard */} ```python """Performance and efficiency testing patterns.""" from mcp_eval import task, Expect import time @task("Test response time") async def test_response_time(agent, session): """Verify responses are generated within acceptable time.""" start_time = time.time() response = await agent.generate_str( "What is 2+2? Give me just the number." ) elapsed = (time.time() - start_time) * 1000 # Convert to ms # Should respond quickly for simple queries await session.assert_that( Expect.performance.response_time_under(2000), # 2 seconds name="quick_response" ) # Content should be concise assert len(response) < 50, "Response should be concise" @task("Test batch processing efficiency") async def test_batch_efficiency(agent, session): """Test efficiency when processing multiple items.""" response = await agent.generate_str( "Calculate the following: " "1) 15 + 27 " "2) 98 - 43 " "3) 12 * 8 " "4) 144 / 12 " "Process all calculations efficiently." ) # Should use tool efficiently await session.assert_that( Expect.tools.count("calculate", expected_count=4), name="batch_processed" ) # Should complete in minimal iterations await session.assert_that( Expect.performance.max_iterations(2), name="efficient_iterations" ) # Verify all results for expected in ["42", "55", "96", "12"]: await session.assert_that( Expect.content.contains(expected), name=f"contains_{expected}", response=response ) @task("Test caching behavior") async def test_caching(agent, session): """Verify caching improves performance on repeated queries.""" # First call - should be slower response1 = await agent.generate_str( "Fetch the current weather in Tokyo" ) metrics1 = session.get_metrics() # Same call - should be faster (cached) response2 = await agent.generate_str( "Fetch the current weather in Tokyo" ) metrics2 = session.get_metrics() # Second call should be faster assert metrics2.total_duration_ms < metrics1.total_duration_ms * 0.5, \ "Cached response should be at least 50% faster" # Content should be consistent await session.assert_that( Expect.judge.llm( "Both responses contain consistent weather information", min_score=0.9 ), name="consistent_cache", response=response2 ) @task("Test parallel tool execution") async def test_parallel_execution(agent, session): """Verify agent can execute independent tools in parallel.""" response = await agent.generate_str( "Simultaneously: " "1) Fetch weather for London " "2) Calculate 99 * 77 " "3) Read contents of readme.txt" ) # Check parallelism in metrics metrics = session.get_metrics() assert metrics.max_concurrent_operations > 1, \ "Should execute tools in parallel" # All tools should complete await session.assert_that( Expect.tools.was_called("weather_api"), Expect.tools.was_called("calculate"), Expect.tools.was_called("file_reader"), name="all_tools_called" ) ``` ## Full integration suite Complete multi-server orchestration testing: ```python """Full integration test suite for multi-server scenarios.""" from mcp_eval import task, setup, Expect from mcp_agent.agents.agent import Agent @setup def configure_integration(): """Setup multi-server test environment.""" # Agent has access to: database, api, filesystem, calculator pass @task("Test data pipeline flow") async def test_data_pipeline(agent, session): """Test complete data processing pipeline.""" response = await agent.generate_str(""" 1. Read user IDs from users.csv 2. For each user, fetch their data from the API 3. Calculate statistics (average age, total count) 4. Store results in the database 5. Generate a summary report """) # Verify complete pipeline execution expected_sequence = [ "file_reader", # Read CSV "api", # Fetch user data "calculator", # Calculate stats "database", # Store results ] await session.assert_that( Expect.tools.sequence(expected_sequence, allow_other_calls=True), name="pipeline_sequence" ) # Verify data integrity await session.assert_that( Expect.judge.llm( rubric=""" Verify the response shows: 1. Successfully read user data 2. Fetched additional info from API 3. Calculated correct statistics 4. Stored in database 5. Generated meaningful summary """, min_score=0.85 ), name="pipeline_complete", response=response ) @task("Test cross-server transaction") async def test_transaction(agent, session): """Test transactional operations across servers.""" response = await agent.generate_str(""" Perform a money transfer: 1. Check balance in account A (database) 2. If sufficient, deduct $100 from account A 3. Add $100 to account B 4. Log transaction to audit.log (filesystem) 5. Send notification via API Ensure atomicity - roll back on any failure """) # Should maintain consistency await session.assert_that( Expect.judge.multi_criteria( criteria={ "atomicity": "Transaction is atomic - all or nothing", "consistency": "Data remains consistent", "audit": "Transaction is properly logged", "notification": "Notification is sent" }, require_all_pass=True ), name="transaction_integrity", response=response ) @task("Test server coordination") async def test_server_coordination(agent, session): """Test complex coordination between multiple servers.""" response = await agent.generate_str(""" Coordinate a backup operation: 1. Stop accepting new writes (database) 2. Flush all pending operations 3. Create filesystem snapshot 4. Upload snapshot to cloud (api) 5. Verify backup integrity 6. Resume normal operations Report on each step. """) # Check coordination metrics metrics = session.get_metrics() # Operations should be sequential for consistency await session.assert_that( Expect.performance.max_concurrent_operations(1), name="sequential_coordination" ) # All steps should complete await session.assert_that( Expect.tools.success_rate(min_rate=1.0), name="all_steps_succeeded" ) ``` ## Dataset-driven testing Systematic testing using datasets for comprehensive coverage: ```python """Dataset-driven testing for comprehensive coverage.""" from mcp_eval import Dataset, Case from mcp_eval.evaluators import ( ToolWasCalled, ResponseContains, LLMJudge, ToolSucceeded ) # Define test cases calculation_cases = [ Case( name="simple_addition", inputs="Calculate 5 + 3", expected_output="8", evaluators=[ ToolWasCalled("calculator"), ResponseContains("8"), ToolSucceeded("calculator") ] ), Case( name="complex_expression", inputs="Calculate (10 * 5) + (20 / 4) - 3", expected_output="52", evaluators=[ ToolWasCalled("calculator", min_times=1), ResponseContains("52"), LLMJudge("Shows correct order of operations") ] ), Case( name="word_problem", inputs="If I have 12 apples and give away 3, how many remain?", expected_output="9", evaluators=[ ResponseContains("9"), LLMJudge("Correctly interprets word problem", min_score=0.8) ] ), ] # Create and run dataset async def run_calculation_tests(): """Run comprehensive calculation tests via dataset.""" dataset = Dataset( name="Calculator Test Suite", cases=calculation_cases, agent_spec="CalculatorAgent" # Reference to configured agent ) # Define how to execute each case async def execute_case(inputs: str, agent, session) -> str: return await agent.generate_str(inputs) # Run evaluation report = await dataset.evaluate(execute_case) # Generate reports report.print(include_scores=True) report.to_json("calculation_results.json") report.to_html("calculation_results.html") # Assertions on overall results assert report.pass_rate >= 0.9, "At least 90% should pass" assert report.get_case("simple_addition").passed, "Basic addition must work" ``` ### Loading datasets from files ```yaml # datasets/api_tests.yaml name: "API Integration Tests" cases: - name: "fetch_json" inputs: "Fetch JSON data from https://api.example.com/users" evaluators: - type: "ToolWasCalled" args: tool_name: "http_client" - type: "ResponseContains" args: text: "users" - type: "LLMJudge" args: rubric: "Successfully fetches and interprets JSON data" - name: "post_data" inputs: "POST {name: 'test'} to https://api.example.com/users" evaluators: - type: "ToolWasCalled" args: tool_name: "http_client" - type: "ToolSucceeded" args: tool_name: "http_client" - name: "handle_404" inputs: "Fetch from https://api.example.com/nonexistent" evaluators: - type: "LLMJudge" args: rubric: "Handles 404 error appropriately" min_score: 0.8 ``` ```python # Load and run the dataset from mcp_eval import Dataset dataset = Dataset.from_yaml("datasets/api_tests.yaml") report = await dataset.evaluate(task_func) ``` ## Advanced patterns ### Custom evaluators ```python """Creating custom evaluators for specific needs.""" from mcp_eval.evaluators.base import SyncEvaluator, EvaluatorContext from mcp_eval.evaluators.shared import EvaluatorResult class SQLQueryValidator(SyncEvaluator): """Validates SQL query syntax and safety.""" def __init__(self, allow_destructive: bool = False): self.allow_destructive = allow_destructive def evaluate_sync(self, ctx: EvaluatorContext) -> EvaluatorResult: response = ctx.output.lower() # Check for SQL injection attempts dangerous_patterns = ["drop table", "delete from", "truncate"] if not self.allow_destructive: for pattern in dangerous_patterns: if pattern in response: return EvaluatorResult( passed=False, expected="Safe SQL query", actual=f"Contains dangerous pattern: {pattern}" ) # Validate basic syntax if "select" in response and "from" in response: return EvaluatorResult( passed=True, expected="Valid SQL query", actual="Query appears valid" ) return EvaluatorResult( passed=False, expected="Valid SQL query", actual="Missing required SQL keywords" ) # Use custom evaluator @task("Test SQL generation") async def test_sql_generation(agent, session): response = await agent.generate_str( "Generate SQL to find all users older than 25" ) await session.assert_that( SQLQueryValidator(allow_destructive=False), name="valid_safe_sql", response=response ) ``` ### Mocking and test doubles ```python """Using mocks for isolated testing.""" from unittest.mock import AsyncMock, patch @task("Test with mocked server") async def test_with_mock(agent, session): """Test agent behavior with mocked server responses.""" # Mock the fetch tool with patch('mcp_agent.tools.fetch') as mock_fetch: mock_fetch.return_value = { "status": 200, "content": "Mocked response data" } response = await agent.generate_str( "Fetch data from https://api.example.com" ) # Verify mock was called assert mock_fetch.called # Check agent handled mocked data await session.assert_that( Expect.content.contains("Mocked response"), response=response ) ``` ## Running the examples ### Command line ```bash # Run all examples (decorator/dataset) mcp-eval run examples/ # Run pytest examples uv run pytest -q examples/mcp_server_fetch/tests/test_pytest_style.py # Run specific test file (decorators) mcp-eval run examples/test_calculator.py # Generate reports (decorators) mcp-eval run examples/ \ --html reports/examples.html \ --json reports/examples.json \ --markdown reports/examples.md ``` ### CI/CD integration ```yaml name: mcp-eval PR Tests on: pull_request: branches: [ "main" ] jobs: tests: permissions: contents: read pull-requests: write issues: write runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Run MCP-Eval id: mcpeval uses: lastmile-ai/mcp-eval/.github/actions/mcp-eval/run with: python-version: '3.11' tests: tests/ run-args: '-v --max-concurrency 4' pr-comment: 'true' set-summary: 'true' upload-artifacts: 'true' commit-reports: 'true' env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ``` {/* TODO: Screenshot of CI/CD test results in GitHub Actions */} ## Tips for writing good examples Use clear, specific prompts that leave no ambiguity about expected behavior Each test should focus on a single aspect or behavior Test names should clearly describe what they're testing Explain complex logic or non-obvious test strategies ## More resources * 📚 [API Reference](./api-catalog) - Complete API documentation * 🎯 [Best Practices](./best-practices) - Testing best practices * 🔧 [Configuration](./configuration) - Advanced configuration options * 🚀 [Common Workflows](./common-workflows) - Step-by-step guides *** **Ready to test?** Copy any example above, adapt it to your needs, and start ensuring your MCP servers and agents work perfectly! 🎉 # Example: mcp_server_fetch Source: https://mcp-eval.ai/examples-mcp-server-fetch Walkthrough of the fetch server example: datasets, golden paths, and assertions. Source: `examples/mcp_server_fetch/` ## Overview This example validates a simple MCP server that exposes a `fetch` tool to retrieve web content. The tests illustrate three styles (decorators, pytest, and legacy assertions) and demonstrate how to combine structural assertions, path constraints, and LLM judges. * Verify the agent calls the `fetch` tool when appropriate * Check extracted content for known signals (e.g., "Example Domain") * Ensure efficient paths (no unnecessary steps) * Evaluate quality with rubric-based judges * Choosing assertions per outcome type (structural, tool, path, judge) * Designing resilient tests using immediate vs deferred checks * Reading metrics and span trees to diagnose behavior ## Structure * `datasets/` – YAML and Python datasets * `tests/` – pytest style, decorators, assertions style * `golden_paths/` – expected sequences * `mcpeval.yaml` – config for provider, reports ## Run ```bash cd examples/mcp_server_fetch mcp-eval run tests/ --markdown test-reports/results.md --html test-reports/index.html ``` ## Assertion design and rationale ### 1) Prove the right tool was used When a prompt requires reading a URL, we assert the `fetch` tool was called: ```python await session.assert_that(Expect.tools.was_called("fetch"), name="fetch_tool_called") ``` Why: catches regressions where the agent “hallucinates” content without making tool calls, or switches to an unintended tool. Tip: combine with `Expect.tools.count("fetch", 1)` to detect duplicate calls. ### 2) Validate output structure rather than brittle text For tool outputs, prefer structural checks over raw substring matching: ```python await session.assert_that( Expect.tools.output_matches( tool_name="fetch", expected_output=r"use.*examples", match_type="regex", case_sensitive=False, field_path="content[0].text", ), name="fetch_output_match", ) ``` Why: tool responses are often nested structures. Field‑scoped, regex/partial checks are stable across formatting differences and small content changes. ### 3) Check content cues in the assistant’s final message After tool use, assert the answer includes expected signals: ```python resp = await agent.generate_str("Fetch https://example.com") await session.assert_that( Expect.content.contains("Example Domain"), response=resp, name="contains_domain_text" ) ``` Why: validates the final user‑visible output, not just tool logs. ### 4) Constrain the path and efficiency For simple fetch tasks, we expect a single `fetch` and minimal steps: ```python await session.assert_that( Expect.path.efficiency( expected_tool_sequence=["fetch"], allow_extra_steps=1, tool_usage_limits={"fetch": 1}, ), name="fetch_path_efficiency", ) ``` Why: detects backtracking, repeated tools, or detours. Combats “thrashing” behaviors. ### 5) Enforce iteration and latency budgets ```python await session.assert_that(Expect.performance.max_iterations(3), name="efficiency_check") await session.assert_that(Expect.performance.response_time_under(10_000)) ``` Why: catches runaway loops and slow paths early. Pairs well with CI budgets. ### 6) Use judges when “quality” is subjective Some checks need subjective evaluation (e.g., “good summary”). Use rubric‑based judges: ```python judge = Expect.judge.llm( rubric="Response should demonstrate successful content extraction and provide a meaningful summary", min_score=0.8, include_input=True, ) await session.assert_that(judge, response=resp, name="extraction_quality_assessment") ``` Why: judges provide a tunable gate (min\_score) for non‑deterministic tasks. In CI, keep them few and scoped. ### 7) Multi‑criteria judges for richer rubrics ```python from mcp_eval.evaluators import EvaluationCriterion criteria = [ EvaluationCriterion(name="accuracy", description="Factual correctness", weight=2.0, min_score=0.8), EvaluationCriterion(name="completeness", description="Covers key points", weight=1.5, min_score=0.7), ] judge_mc = Expect.judge.multi_criteria(criteria, aggregate_method="weighted", use_cot=True) await session.assert_that(judge_mc, response=resp, name="multi_criteria") ``` Why: breaks down quality into interpretable dimensions, enabling targeted improvements. ## Immediate vs deferred: how these fit together * Immediate: content/judge checks that rely on `response` * Deferred: tools/path/performance checks that need session metrics Design tip: make immediate assertions small and concrete; keep most structural checks deferred for stability. ## What it demonstrates * Fetch tool end‑to‑end scenarios * Dataset style configs and generated cases * Tool sequence and output matching * Judge rubric for quality checks > Placeholder: add screenshots of the HTML report for a passing run and for a failure showing a mismatched tool output. # FAQ Source: https://mcp-eval.ai/faq Frequently asked questions about mcp-eval, servers, agents, and configuration. ### Does server language matter? No. Any language works as long as your server implements MCP. mcp-eval drives it via the agent and MCP transport. ### How do I add servers quickly? Use `mcp-eval server add` or import from `mcp.json` / DXT. See [Quickstart](./quickstart) and [CLI Reference](./cli-reference). ### Decorator order? Place `@with_agent(...)` above `@task(...)` when used together. ### Can I run without an LLM? Yes for tool/structural checks that don’t require generation. LLM judge assertions need an LLM. ### How do I run a specific test or dataset? `mcp-eval run tests/test_file.py::test_func` or `mcp-eval run dataset path.yaml`. ### Where are configs discovered? [config.py](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/config.py) documents discovery for `mcpeval.yaml` and secrets. {/* TODO: Add a short video/GIF explaining the high-level workflow and where each file lives. */} ### Parallel datasets? `Dataset.evaluate(..., max_concurrency=N)` enables parallel case execution. # mcp-eval Documentation Source: https://mcp-eval.ai/index The comprehensive testing framework for MCP servers and tool-using agents. **Your flight simulator for MCP servers and agents** — Connect agents to real MCP servers, run realistic scenarios, and calculate metrics for tool calls and more. [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro) standardizes how applications provide context to large language models (LLMs). Think of MCP like a USB-C port for AI applications. **`mcp-eval`** ensures your MCP servers, and agents built with them, work reliably in production. ## What `mcp-eval` Does for You Ensure your MCP servers respond correctly to agent requests and handle edge cases gracefully Measure how effectively agents use tools, follow instructions, and recover from errors Monitor latency, token usage, cost, and success rates with OpenTelemetry-backed metrics Use structural checks, LLM judges, and path efficiency validators to ensure high quality ## Get Started in 30 Seconds We recommend using [uv](https://docs.astral.sh/uv/): ```bash uv (recommended) # Install mcp-eval globally (for CLI) uv tool install mcpevals # Add mcp-eval dependency to your project uv add mcpevals # Initialize your project (interactive setup) mcp-eval init # Add your MCP server to test mcp-eval server add # Auto-generate tests with an LLM mcp-eval generate # Run decorator/dataset tests mcp-eval run tests/ # Run pytest tests (use pytest) uv run pytest -q tests ``` ```bash pip # Install mcp-eval pip install mcpevals # Initialize your project mcp-eval init # Add your MCP server mcp-eval server add # Run decorator/dataset tests mcp-eval run tests/ # Run pytest tests (use pytest) pytest -q tests ``` **Test any MCP server:** It doesn't matter what language your MCP server is written in - Python, TypeScript, Go, Rust, Java, or any other. As long as it implements the MCP protocol, `mcp-eval` can test it! You're ready to start testing! [Continue with the Quickstart →](./quickstart) ## 🎮 Choose Your Testing Adventure What are you evaluating today? **You built an MCP server** (in any language!) and want to ensure it handles agent requests correctly. mcp-eval will spin up an AI agent to test your server with realistic requests, edge cases, and error scenarios. **Your server could be:** * A streamable HTTP database connector * An SSE API wrapper * A stdio file system server * Any server that speaks MCP! MCP Server Testing Guide Testing the Fetch Server **You built an AI agent** that uses MCP servers and want to ensure it uses tools effectively. mcp-eval will connect your agent to MCP servers and verify it uses tools correctly, handles errors, and meets performance targets. **Your agent could be:** * A customer service bot * A coding assistant * A deep research agent * Any MCP agent! Agent Evaluation Guide Common Testing Patterns **You're building a complete system** with both MCP servers and agents. mcp-eval can test your entire integration - ensuring servers handle requests correctly AND agents use tools effectively. 5-minute setup Core concepts Browse all examples ## Why Teams Choose `mcp-eval` * **Production-readiness**: Built on OpenTelemetry for enterprise-grade observability * **Multiple test styles**: Choose between decorators, pytest, or dataset-driven testing * **Rich assertions**: Content checks, tool verification, performance gates, and LLM judges * **CI/CD friendly**: GitHub Actions support, JSON/HTML reports, and regression detection * **Language agnostic**: Test MCP servers written in any language ## Quick Navigation Get up and running in 5 minutes Step-by-step guides for typical tasks Complete assertion catalog and APIs ## Learning Path Understand `mcp-eval`'s architecture and philosophy Your first test in 5 minutes Core concepts and terminology The unified Expect API for all assertions Practical testing patterns AI-powered test creation Testing MCP server implementations Measuring agent effectiveness Systematic evaluation suites Settings and customization GitHub Actions and automation Understanding test outputs Complete command documentation Detailed API documentation Common issues and solutions Frequently asked questions ## Example: Your First Test ```python test_fetch.py from mcp_eval import task, Expect @task("Verify fetch server works correctly") async def test_fetch(agent, session): # Ask the agent to fetch a webpage response = await agent.generate_str("Fetch https://example.com and summarize it") # Assert the right tool was called await session.assert_that(Expect.tools.was_called("fetch")) # Verify the content is correct await session.assert_that(Expect.content.contains("Example Domain"), response=response) # Check performance await session.assert_that(Expect.performance.response_time_under(5000)) ``` ```python pytest_style.py import pytest from mcp_eval import create_agent, Expect @pytest.mark.asyncio async def test_fetch_with_pytest(): agent = await create_agent("claude-3-5-sonnet") response = await agent.generate_str("Fetch https://example.com") assert "Example Domain" in response assert agent.tools_called == ["fetch"] ``` [See more examples →](./examples) ## Join the Community Report issues and contribute Get help and share experiences # Integrations Source: https://mcp-eval.ai/integrations Patterns for connecting external MCP servers and multi‑server agents. ### Popular patterns * SaaS SSE/HTTP servers for product data (e.g., docs, tickets) * Local stdio servers for CLIs/databases * Composite agents with multiple servers (fetch, search, storage) ### Multi‑server agents Combine multiple servers on a single agent and constrain path with assertions: ```python from mcp_eval.catalog import Expect await session.assert_that(Expect.tools.sequence(["search", "fetch"])) await session.assert_that(Expect.path.efficiency(expected_tool_sequence=["search","fetch"])) ``` ### Server discovery and drift * Use `mcp-eval server list -v` in CI to detect missing tools. * Add an `Expect.tools.count(tool, >=1)` guard to ensure expected calls happen. ### Security * Store credentials in CI secrets / local secrets file. * Avoid writing secrets in `mcp.json` or DXT. # Metrics & Tracing Source: https://mcp-eval.ai/metrics-tracing OTEL traces as source of truth — metrics, span tree, coverage, and where to find artifacts. ### Traces → metrics The session writes OTEL spans to JSONL; mcp-eval converts them to rich metrics: * Tool calls (names, args, times, errors) * Iteration count, response latency * Token and cost estimates * Tool coverage by server (available vs used) Sources: * [session.py](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/session.py) * [metrics.py](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/metrics.py) * [span\_tree.py](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/otel/span_tree.py) ### Span tree analysis `SpanTree` enables: * LLM rephrasing loop detection * Inefficient tool paths analysis * Error recovery sequences ### Artifacts * Traces: `./test-reports/*.jsonl` * Per‑test JSON results: `./test-reports/*_results.json` * Combined JSON/Markdown/HTML (via runner options) {/* TODO: Add screenshot snippets of span tree and metrics table (LLM/tool time, token counts). */} # mcp-eval Overview Source: https://mcp-eval.ai/overview Learn about mcp-eval, the comprehensive framework for testing MCP servers and tool-using agents in production-like environments. > mcp-eval is your "flight simulator" for tool-using LLMs. Connect agents to real MCP servers, run realistic scenarios, and get production-grade insights into behavior and performance. ## What is mcp-eval? mcp-eval is a developer-first evaluation framework designed specifically for testing Model Context Protocol (MCP) servers and the agents that use them. Unlike traditional testing approaches that mock interactions or test components in isolation, mcp-eval exercises your complete system in the environment it actually runs in: an LLM/agent calling real MCP tools. **Think of it this way**: If unit tests are like testing car parts on a bench, mcp-eval is like taking the whole car to a test track. You see how everything works together under realistic conditions. ## Why mcp-eval exists ### The challenge As AI agents become more sophisticated and MCP servers proliferate, teams face critical questions: * **For MCP server developers**: "Will my server handle real agent requests correctly? What about edge cases?" * **For agent developers**: "Is my agent using tools effectively? Does it recover from errors?" * **For both**: "How do we measure quality, performance, and reliability before production?" ### The solution mcp-eval addresses these challenges by providing: 1. **Real environment testing** - No mocks, actual agent-to-server communication 2. **Full observability** - OpenTelemetry traces capture detailed agent execution to run evals over 3. **Rich assertion library** - From tool checks to sophisticated path analysis 4. **Multiple test styles** - Choose what fits your workflow -- `pytest`, datasets or `@task` decorators. 5. **Language agnostic** - Test MCP servers written in any language ## Core capabilities **Content validation**: Pattern matching, regex, contains/not-contains **Tool verification**: Was called, call counts, arguments, outputs **Performance gates**: Response time, iteration limits, token usage **Quality judges**: LLM-based evaluation with custom rubrics **Path analysis**: Efficiency, backtracking, optimal sequences **Automatic capture**: Every tool call, LLM interaction, timing **Span tree analysis**: Visualize execution flow and bottlenecks **Cost tracking**: Token usage and estimated costs per test **Performance breakdown**: LLM time vs tool time vs overhead **Error recovery**: Track retry patterns and failure handling **Decorator style**: Simple `@task` decorators for quick tests **Pytest integration**: Use familiar pytest fixtures and markers (run with `uv run pytest`) **Dataset driven**: Systematic evaluation with test matrices **AI generation**: Let Claude/GPT generate test scenarios **Parameterization**: Test variations with minimal code **Quick start**: `mcp-eval init` sets up everything **Smart CLI**: Discover servers, generate tests, validate config **Rich reports**: Console, JSON, Markdown, interactive HTML **CI/CD ready**: GitHub Actions, exit codes, artifact uploads **Helpful diagnostics**: `doctor` and `validate` commands ## How mcp-eval works ### Architecture overview ```mermaid graph LR A[Test Suite] --> B[mcp-eval] B --> C[Agent] C --> D[MCP Servers] D --> C C --> B B --> E[OTEL Traces] E --> F[Metrics] E --> G[Reports] F --> H[Assertions] G --> I[CI/CD] ``` ### Execution flow Define which MCP servers are available and configure your agent with appropriate tools and instructions. Create tests that give your agent realistic tasks requiring tool use. mcp-eval orchestrates the agent, captures all interactions via OpenTelemetry, and records comprehensive traces. OTEL traces are parsed to extract tool calls, timings, token usage, error patterns, and execution paths. Your assertions run against the response content and extracted metrics to verify behavior. Results are compiled into multiple formats for different audiences and use cases. ## A complete example Here's a real-world test that showcases mcp-eval's capabilities: ```python from mcp_eval import task, setup, Expect from mcp_eval.evaluators import EvaluationCriterion @setup def configure(): # Define the agent and servers it can use mcp_eval.use_agent( AgentSpec( name="weather_assistant", instruction="You help users with weather information.", server_names=["weather_api", "location_service"], model="claude-3-5-haiku-20241022" ) ) @task("Get weather for user's location") async def test_weather_flow(agent, session): # Execute the task response = await agent.generate_str( "What's the weather like in San Francisco tomorrow?" ) # Verify tool usage - immediate checks await session.assert_that( Expect.tools.was_called("get_location"), name="location_service_called" ) await session.assert_that( Expect.tools.was_called("get_weather_forecast"), name="weather_api_called" ) # Check content quality await session.assert_that( Expect.content.contains("San Francisco"), response=response, name="mentions_city" ) await session.assert_that( Expect.content.regex(r"\d+\s*°[CF]"), # Temperature pattern response=response, name="includes_temperature" ) # Verify execution efficiency await session.assert_that( Expect.tools.sequence( ["get_location", "get_weather_forecast"], allow_other_calls=False ), name="optimal_tool_sequence" ) # Apply quality judge await session.assert_that( Expect.judge.multi_criteria([ EvaluationCriterion( name="accuracy", description="Weather info is plausible and specific", weight=2.0, min_score=0.8 ), EvaluationCriterion( name="helpfulness", description="Response is clear and actionable", weight=1.5, min_score=0.7 ) ]), response=response, name="quality_assessment" ) # Performance requirements await session.assert_that( Expect.performance.response_time_under(10000), # 10 seconds name="response_time_check" ) ``` ## Key features in depth ### 🎯 Unified assertion API All assertions use a single, discoverable API pattern: ```python await session.assert_that(Expect.category.specific_check(...)) ``` This provides IDE autocomplete and makes the API easy to explore. ### 📡 OpenTelemetry integration Every interaction generates detailed traces: * Tool invocations with arguments and results * LLM calls with token counts * Timing breakdowns for each operation * Error and retry patterns * Nested span relationships These traces become your single source of truth for debugging and analysis. ### 🤖 Multiple test styles Choose the approach that fits your team: ```python @task("Simple and expressive") async def test_something(agent, session): response = await agent.generate_str("Do something") await session.assert_that(Expect.content.contains("done")) ``` ```python @pytest.mark.asyncio async def test_with_pytest(mcp_agent): response = await mcp_agent.generate_str("Do something") assert "done" in response ``` ```python dataset = Dataset( name="Comprehensive suite", cases=[ Case("test_1", inputs="Do X", evaluators=[...]), Case("test_2", inputs="Do Y", evaluators=[...]) ] ) ``` ### 📈 Production-ready reporting Generate reports in multiple formats: * **Console**: Real-time progress and summaries * **JSON**: Machine-readable for CI/CD pipelines * **Markdown**: PR comments and documentation * **HTML**: Interactive exploration with filtering All reports include links to detailed OTEL traces for debugging. ## When to use mcp-eval ### Perfect for ✅ **MCP server development** - Ensure your server handles agent requests correctly ✅ **Agent development** - Verify your agent uses tools effectively ✅ **Integration testing** - Test agent + server combinations before deployment ✅ **Regression testing** - Catch breaking changes in CI/CD ✅ **Performance optimization** - Identify bottlenecks and inefficiencies ✅ **Quality gating** - Enforce standards before merging code ### Not designed for ❌ **Unit testing** - Use standard testing frameworks for isolated functions ❌ **Load testing** - Consider specialized tools for high-volume testing ❌ **Security testing** - Use dedicated security scanning tools ## Next steps Ready to start testing? Here's your path: Get your first test running in 5 minutes Learn testing patterns and best practices Browse real-world test implementations Explore the complete assertion catalog ## Technical foundations mcp-eval is built on solid technical foundations: * **Async-first Python** for performance and concurrency * **OpenTelemetry** for vendor-neutral observability * **Pydantic** for configuration and validation * **Rich CLI** powered by Click and Rich libraries * **Extensible architecture** for custom evaluators and reporters For implementation details, see our [GitHub repository](https://github.com/lastmile-ai/mcp-eval). # Pytest Source: https://mcp-eval.ai/pytest Use native pytest with mcp-eval fixtures and markers. ### How to run Use native pytest to execute pytest-style tests. With uv: ```bash uv run pytest -q tests # Or a single file/function uv run pytest tests/test_pytest_style.py -v uv run pytest tests/test_pytest_style.py::test_basic_fetch -v ``` Note: `mcp-eval run` executes decorator- and dataset-style tests. Use `pytest` for plain pytest tests. ### Fixtures * `mcp_session`: access the session (metrics, assertions) * `mcp_agent`: a `TestAgent` bound to the session Source: [pytest\_plugin.py](https://github.com/lastmile-ai/mcp-eval/blob/main/src/mcp_eval/pytest_plugin.py) ### Markers * `@pytest.mark.mcp_agent()`: per‑test agent override * `@pytest.mark.network`, `@pytest.mark.slow` ### Example ```python import pytest from mcp_eval import Expect @pytest.mark.asyncio @pytest.mark.network async def test_basic_fetch(mcp_agent): resp = await mcp_agent.generate_str("Fetch https://example.com") await mcp_agent.session.assert_that(Expect.tools.was_called("fetch"), response=resp) await mcp_agent.session.assert_that(Expect.content.contains("Example Domain"), response=resp) ``` Full examples: [test\_pytest\_style.py](https://github.com/lastmile-ai/mcp-eval/blob/main/examples/mcp_server_fetch/tests/test_pytest_style.py) ### Per-test agent override ```python import pytest from mcp_eval import Expect from mcp_agent.agents.agent import Agent @pytest.mark.asyncio @pytest.mark.mcp_agent(Agent(name="custom", instruction="You fetch", server_names=["fetch"])) async def test_with_custom_agent(mcp_agent): resp = await mcp_agent.generate_str("Fetch https://example.com") await mcp_agent.session.assert_that(Expect.tools.was_called("fetch"), response=resp) ``` ### Parametrization ```python @pytest.mark.asyncio @pytest.mark.parametrize( "url,expected", [ ("https://example.com", "Example Domain"), ("https://httpbin.org/html", "Herman Melville"), ], ) async def test_urls(mcp_agent, url, expected): resp = await mcp_agent.generate_str(f"Fetch {url}") await mcp_agent.session.assert_that(Expect.content.contains(expected, case_sensitive=False), response=resp) ``` {/* TODO: Add a larger end-to-end pytest screenshot: network marker, slow marker, and verbose summary. */} #### Parametrization Use standard `@pytest.mark.parametrize` for breadth, or mix with mcp-eval’s `@parametrize` in decorator tests. {/* TODO: Add a screenshot of a pytest run with markers, including skipped slow tests. */} # Quickstart Guide Source: https://mcp-eval.ai/quickstart Get `mcp-eval` up and running in 5 minutes. Learn to install, configure, and run your first tests for MCP servers and agents. > 🚀 **Welcome to `mcp-eval`!** You're about to supercharge your MCP development with powerful testing capabilities. This guide will have you testing MCP servers and agents in just 5 minutes! ## What you'll learn By the end of this quickstart, you'll be able to: * ✅ Install and configure `mcp-eval` for your project * ✅ Connect your MCP servers for testing * ✅ Write and run your first test * ✅ Understand test reports and iterate on failures * ✅ Choose the right testing style for your needs **Time to complete:** \~5 minutes ## Before you begin Let's make sure you have everything ready: ### System requirements Required for running `mcp-eval` [Download Python →](https://www.python.org/downloads/) Any MCP-compatible server to test [Browse MCP servers →](https://github.com/modelcontextprotocol/servers) Claude or OpenAI key for LLM features [Get Claude API →](https://console.anthropic.com/) **New to MCP?** No worries! Check out the [MCP documentation](https://modelcontextprotocol.io) to understand the basics of Model Context Protocol servers. You'll be testing them like a pro in no time! ## Your 5-minute journey to testing mastery {/* TODO: Add animated GIF showing the entire quickstart flow from install to first passing test */} First, let's get `mcp-eval` installed for your project. We recommend using [uv](https://docs.astral.sh/uv/) to install `mcp-eval` as a global tool: ```bash uv tool install mcpevals ``` This makes the `mcp-eval` CLI available globally on your system. **Language agnostic testing:** `mcp-eval` can test MCP servers written in **any language** - Python, TypeScript, Go, Rust, Java, etc. As long as your server implements the MCP protocol, mcp-eval can test it! Next, add mcp-eval as a dependency for your project: # Using uv in a project ```bash uv add mcpevals ``` Alternatively: ```bash pip install mcpevals ``` Now set up your API key for the best experience: ```bash # We recommend Claude for superior test generation and judging export ANTHROPIC_API_KEY="sk-ant-..." # Alternative: OpenAI export OPENAI_API_KEY="sk-..." ``` **Pro tip:** Claude Sonnet or Opus models provide the best results for test generation and LLM judge evaluations! {/* TODO: Screenshot showing successful installation output */} Let's set up your testing environment with our interactive wizard: ```bash mcp-eval init ``` This friendly wizard will: * 🎯 Ask for your preferred LLM provider and model * 📝 Create `mcpeval.yaml` with your configuration * 🔐 Set up `mcpeval.secrets.yaml` for secure API key storage * 🤖 Help you define your first test agent * 🔧 Import any existing MCP servers **What happens during init:** ``` ? Select your LLM provider: Anthropic ? Select model: claude-3-5-sonnet-20241022 ? Import servers from mcp.json? Yes ? Path to mcp.json: .cursor/mcp.json ✓ Found 2 servers: fetch, filesystem ? Create a default agent? Yes ? Agent name: TestBot ? Agent instruction: You test MCP servers thoroughly ✓ Configuration saved to mcpeval.yaml ✓ Secrets saved to mcpeval.secrets.yaml ``` {/* TODO: Screenshot of the interactive init prompts and successful completion */} {/* TODO: Screenshot showing the created files in the project directory */} Before we can test an MCP server, you need to tell mcp-eval how to connect to it. Connection works over any supported [transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) (stdio, websocket, sse, streamable\_http). You can import server configurations from [`mcp.json`](https://code.visualstudio.com/docs/copilot/customization/mcp-servers) or [`dxt`](https://github.com/anthropics/dxt/blob/main/README.md) files, or specify them interactively using the `mcp-eval server add` command. ### Adding your MCP server You have several ways to add a server to your configuration: The easiest way - let mcp-eval guide you: ```bash mcp-eval server add ``` This will prompt you for: * How to add (interactive, from-mcp-json, or from-dxt) * Server name (e.g., "fetch") * Command to run (e.g., "uvx mcp-server-fetch") * Any arguments or environment variables Example interaction: ``` ? How would you like to add the server? interactive ? Server name: fetch ? Command: uvx mcp-server-fetch ? Add environment variables? No ✓ Added server 'fetch' ``` If you're using Cursor or VS Code with MCP: ```bash mcp-eval server add --from-mcp-json .cursor/mcp.json ``` This imports servers from your IDE's MCP configuration. The tool will: 1. Read the mcp.json file 2. Show you available servers 3. Let you choose which ones to add ```bash mcp-eval server add --from-dxt ~/Desktop/my-server.dxt ``` This imports servers from DXT manifest files. Edit `mcpeval.yaml` directly: ```yaml mcp: servers: # Example: fetch server fetch: command: "uvx" args: ["mcp-server-fetch"] env: UV_NO_PROGRESS: "1" # Example: filesystem server filesystem: command: "npx" args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"] # Example: custom streamable http server local_http: description: "A streamable HTTP server." transport: streamable_http url: http://0.0.0.0:3156/mcp headers: my-header: "some_value" ``` See the [Configuration Guide](./configuration#server-configuration) for all options. ### Common server examples Here are some popular MCP servers you might want to test: ```bash fetch # Fetch server (web content) uvx mcp-server-fetch ``` ```bash filesystem # Filesystem server npx -y @modelcontextprotocol/server-filesystem /path/to/directory ``` ```bash github # GitHub server npx -y @modelcontextprotocol/server-github ``` **Verify your server configuration:** After adding a server, you can verify it's working: ```bash # List all configured servers mcp-eval server list # Validate server connectivity mcp-eval validate ``` {/* TODO: Screenshot showing successful server import with list of discovered tools */} Time for the exciting part - running your first test! We'll use the included fetch server example to demonstrate. **Example structure:** The examples assume you have the fetch server configured. If you're testing a different server, you'll need to adjust the test code accordingly. First, let's make sure we have an example test. If you used `mcp-eval init`, you might already have one. Otherwise, let's run: ```bash mcp-eval run examples/mcp_server_fetch/tests/test_decorator_style.py \ -v \ --markdown test-reports/results.md \ --html test-reports/index.html ``` **What's happening:** * 🏃 Running decorator-style tests from the example file * 📊 Verbose output (`-v`) shows test progress * 📝 Markdown report for documentation * 🌐 HTML report for interactive exploration **Expected output:** ``` Running tests... ✓ test_basic_fetch_decorator - Test basic URL fetching [2.3s] ✓ fetch_tool_called: Tool 'fetch' was called ✓ contains_domain_text: Content contains "Example Domain" ✓ fetch_success_rate: Tool success rate 100% ✓ test_content_extraction_decorator - Test extraction quality [3.1s] ✓ fetch_called_for_extraction: Tool 'fetch' was called ✓ extraction_quality_assessment: LLM judge score 0.92 Results: 2 passed, 0 failed Reports saved to test-reports/ ``` {/* TODO: Screenshot of terminal showing colorized test run output with progress */} {/* TODO: Screenshot of the test summary showing passed/failed counts */} Open your shiny new test report to see the details: ```bash # Open the HTML report in your browser open test-reports/index.html # Or view the markdown report cat test-reports/results.md ``` **Understanding the HTML report:** The interactive report shows: * 📊 **Overview dashboard** - Pass/fail rates, performance metrics * 🔍 **Test details** - Each test with all assertions * 🛠️ **Tool usage** - What tools were called and when * 💭 **LLM reasoning** - The agent's thought process * ⚡ **Performance** - Response times and efficiency metrics * 🎯 **Failed assertions** - Detailed diffs and explanations **Common things to check:** * Did the right tools get called? * Was the output accurate? * How efficient was the agent's approach? * What was the LLM judge's assessment? {/* TODO: Screenshot of HTML report overview page showing test results summary */} {/* TODO: Screenshot of a failed assertion with detailed diff view */} {/* TODO: Screenshot of tool usage timeline visualization */} **Test failed?** Don't worry! Check the assertion details to understand why. Common issues: * Tool not found (check server configuration) * Content mismatch (adjust your assertions) * Timeout (increase timeout in config) ## What's next? Write your own test! Now that you've run the example, let's write your very first custom test: ### Choose your testing style **Best for:** Quick, readable tests ```python from mcp_eval import task, Expect @task("My first test") async def test_my_server(agent, session): response = await agent.generate_str( "Use my tool to do something" ) await session.assert_that( Expect.tools.was_called("my_tool"), response=response ) ``` **Best for:** Integration with existing pytest suites ```python import pytest from mcp_eval import Expect @pytest.mark.asyncio async def test_my_server(mcp_agent): response = await mcp_agent.generate_str( "Use my tool to do something" ) await mcp_agent.session.assert_that( Expect.tools.was_called("my_tool"), response=response ) ``` Run with uv: ```bash uv run pytest -q tests # or a single file/function uv run pytest tests/test_my_server.py -v uv run pytest tests/test_my_server.py::test_my_server -v ``` ### Your test file structure Create a new test file `tests/test_my_server.py`: ```python """Tests for my awesome MCP server.""" from mcp_eval import task, setup, Expect @setup def configure_tests(): """Any setup needed before tests run.""" print("🚀 Starting my server tests!") @task("Test basic functionality") async def test_basic_operation(agent, session): """Verify the server responds correctly to basic requests.""" # 1. Send a prompt to the agent response = await agent.generate_str( "Please use the calculator to add 2 + 2" ) # 2. Check that the right tool was called await session.assert_that( Expect.tools.was_called("calculate"), name="calculator_used" ) # 3. Verify the response content await session.assert_that( Expect.content.contains("4"), name="correct_answer", response=response ) # 4. Check efficiency (optional) await session.assert_that( Expect.performance.max_iterations(3), name="completed_efficiently" ) @task("Test error handling") async def test_error_recovery(agent, session): """Verify graceful error handling.""" response = await agent.generate_str( "Try to divide by zero, then recover" ) # Use LLM judge for complex behavior await session.assert_that( Expect.judge.llm( rubric="Agent should handle error gracefully and provide helpful response", min_score=0.8 ), name="error_handling_quality", response=response ) ``` Run your new test: ```bash mcp-eval run tests/test_my_server.py -v --html reports/my_test.html ``` ## Troubleshooting common issues **Solution:** Check your `mcpeval.yaml` to ensure the server is properly configured: ```yaml mcp: servers: my_server: command: "python" args: ["path/to/server.py"] ``` Also verify the server name matches what you're using in your agent's `server_names`. **Solution:** Increase the timeout in your configuration: ```yaml execution: timeout_seconds: 600 # 10 minutes ``` **Solution:** Ensure your API key is set correctly: ```bash # Check if it's set echo $ANTHROPIC_API_KEY # Or add to mcpeval.secrets.yaml anthropic: api_key: "sk-ant-..." ``` ## Resources to level up Ready to become a `mcp-eval` expert? Here's your learning path: Full test suites showing all testing patterns Step-by-step guides for typical testing scenarios Deep dive into all configuration options Pro tips for writing maintainable tests ## Get help * 💬 **Questions?** Check our [FAQ](./faq) or [troubleshooting guide](./troubleshooting) * 🐛 **Found a bug?** [Report it on GitHub](https://github.com/lastmile-ai/mcp-eval/issues) * 💡 **Have ideas?** We'd love to hear them in [discussions](https://github.com/lastmile-ai/mcp-eval/discussions) *** **Congratulations!** 🎉 You've successfully set up `mcp-eval` and run your first tests. You're now ready to ensure your MCP servers and agents work flawlessly. Happy testing! # Reports Source: https://mcp-eval.ai/reports Console, Markdown, and HTML reports with metrics, assertion outcomes, and traces. ## Output formats * Console summary (always) * Markdown: portable report for PRs/wikis * HTML: rich, filterable report with per‑test detail ```bash mcp-eval run tests/ --json test-reports/results.json --markdown test-reports/results.md --html test-reports/index.html ``` ## What’s included * Test metadata: id, name, agent, servers * Assertions: pass/fail with messages and parameters * Judge scores: rubric, criterion scores, aggregate * Tool usage: sequence, counts, success rate * Performance: response time, iterations, duration * Metrics: coverage and OTEL identifiers ## Artifacts directory Default directory: `./test-reports` (configurable via reporting settings) Artifacts may include: * `results.json` – full machine‑readable results * `results.md` – Markdown summary suitable for GitHub * `index.html` – rich HTML report * Per‑test trace files (if enabled) ## Configuration In `mcpeval.yaml`: ```yaml reporting: formats: ["json", "markdown", "html"] output_dir: "./test-reports" include_traces: true ``` ## In CI * Upload `test-reports/` as build artifacts. * Post `results.md` into PR comments. * Fail the job on `all_passed == false`. > Placeholder: add screenshots of the HTML report summary and a failed assertion detail view. # Security Guide Source: https://mcp-eval.ai/security Secure your mcp-eval testing environment. Learn about API key management, secure configurations, compliance, and security best practices. > 🔒 **Security first!** This guide covers essential security practices for mcp-eval, from protecting API keys to ensuring compliance with security standards. Keep your tests and data safe. ## Security quick reference Jump to what you need: Secure credential handling Encrypt and protect test data Secure communications Authentication and authorization Meet regulatory requirements Handle security incidents ## API key management ### Never commit secrets **🚨 Critical:** Never commit API keys to version control! ```bash # .gitignore - ALWAYS include these mcpeval.secrets.yaml mcp-agent.secrets.yaml .env .env.local *.key *.pem secrets/ .anthropic/ .openai/ ``` ### Secure storage options #### Environment variables (basic) ```bash # Set in shell profile (~/.bashrc, ~/.zshrc) export ANTHROPIC_API_KEY="sk-ant-..." export OPENAI_API_KEY="sk-..." # Never log or print these! echo $ANTHROPIC_API_KEY # DON'T DO THIS ``` #### Secrets file (better) ```yaml # mcpeval.secrets.yaml anthropic: api_key: "sk-ant-..." # Encrypted at rest # Set file permissions (Unix/Linux) chmod 600 mcpeval.secrets.yaml # Owner read/write only ``` #### Secret management systems (best) ```python # integrations/aws_secrets.py import boto3 import json class AWSSecretManager: """Fetch secrets from AWS Secrets Manager.""" def __init__(self, region='us-east-1'): self.client = boto3.client('secretsmanager', region_name=region) def get_api_key(self, secret_name): """Retrieve API key from AWS Secrets Manager.""" try: response = self.client.get_secret_value(SecretId=secret_name) secret = json.loads(response['SecretString']) return secret['api_key'] except Exception as e: # Log error without exposing secret print(f"Failed to retrieve secret: {type(e).__name__}") raise # Use in configuration from mcp_eval.config import set_settings secret_mgr = AWSSecretManager() anthropic_key = secret_mgr.get_api_key('mcp-eval/anthropic') set_settings({ 'anthropic': {'api_key': anthropic_key} }) ``` ```python # integrations/vault_secrets.py import hvac class VaultSecretManager: """Fetch secrets from HashiCorp Vault.""" def __init__(self, vault_url, token): self.client = hvac.Client(url=vault_url, token=token) def get_api_key(self, path): """Retrieve API key from Vault.""" response = self.client.secrets.kv.v2.read_secret_version( path=path ) return response['data']['data']['api_key'] # Use in configuration vault = VaultSecretManager( vault_url='https://vault.company.com', token=os.environ['VAULT_TOKEN'] ) anthropic_key = vault.get_api_key('secret/mcp-eval/anthropic') ``` ```python # integrations/azure_secrets.py from azure.keyvault.secrets import SecretClient from azure.identity import DefaultAzureCredential class AzureSecretManager: """Fetch secrets from Azure Key Vault.""" def __init__(self, vault_url): credential = DefaultAzureCredential() self.client = SecretClient( vault_url=vault_url, credential=credential ) def get_api_key(self, secret_name): """Retrieve API key from Azure Key Vault.""" secret = self.client.get_secret(secret_name) return secret.value # Use in configuration azure_vault = AzureSecretManager( vault_url='https://myvault.vault.azure.net/' ) anthropic_key = azure_vault.get_api_key('anthropic-api-key') ``` ### API key rotation Implement regular key rotation: ```python # security/key_rotation.py import datetime import secrets class APIKeyRotation: """Manage API key rotation.""" def __init__(self, secret_manager): self.secret_manager = secret_manager def should_rotate(self, key_name, max_age_days=90): """Check if key needs rotation.""" metadata = self.secret_manager.get_metadata(key_name) created_date = metadata['created_date'] age = (datetime.now() - created_date).days return age > max_age_days def rotate_key(self, key_name, provider): """Rotate an API key.""" # Generate new key from provider new_key = provider.generate_new_key() # Store new key self.secret_manager.update_secret(key_name, new_key) # Deactivate old key (after grace period) provider.schedule_deactivation(old_key, grace_hours=24) # Log rotation (without exposing keys) self.log_rotation(key_name) return new_key ``` ## Data protection ### Encrypt sensitive test data ```python # security/encryption.py from cryptography.fernet import Fernet import json import base64 class TestDataEncryption: """Encrypt sensitive test data.""" def __init__(self, key=None): if key: self.cipher = Fernet(key) else: # Generate new key self.cipher = Fernet(Fernet.generate_key()) def encrypt_test_data(self, data): """Encrypt test data.""" json_str = json.dumps(data) encrypted = self.cipher.encrypt(json_str.encode()) return base64.b64encode(encrypted).decode() def decrypt_test_data(self, encrypted_data): """Decrypt test data.""" decoded = base64.b64decode(encrypted_data) decrypted = self.cipher.decrypt(decoded) return json.loads(decrypted.decode()) # Usage in tests encryptor = TestDataEncryption() # Encrypt sensitive test inputs sensitive_data = { "user_id": "12345", "ssn": "123-45-6789", "credit_card": "4111-1111-1111-1111" } encrypted = encryptor.encrypt_test_data(sensitive_data) # Use encrypted data in test @task("Test with encrypted data") async def test_sensitive_operation(agent, session): # Decrypt only when needed data = encryptor.decrypt_test_data(encrypted) # Use data in test response = await agent.generate_str( f"Process user {data['user_id']}" ) # Clear sensitive data from memory del data ``` ### Sanitize test outputs ```python # security/sanitization.py import re class OutputSanitizer: """Sanitize sensitive information from test outputs.""" # Patterns for sensitive data PATTERNS = { 'api_key': r'(sk-[a-zA-Z0-9]{48})', 'email': r'([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})', 'ssn': r'\b(\d{3}-\d{2}-\d{4})\b', 'credit_card': r'\b(\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4})\b', 'ip_address': r'\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b', } def sanitize(self, text): """Remove sensitive information from text.""" sanitized = text for name, pattern in self.PATTERNS.items(): sanitized = re.sub( pattern, f'[REDACTED_{name.upper()}]', sanitized ) return sanitized # Use in test reporting sanitizer = OutputSanitizer() @task("Test with sanitized output") async def test_with_sanitization(agent, session): response = await agent.generate_str("Get user data") # Sanitize before logging or reporting safe_response = sanitizer.sanitize(response) print(f"Response: {safe_response}") # Original response for assertions await session.assert_that( Expect.content.contains("user_id"), response=response ) ``` ### Secure file handling ```python # security/secure_files.py import tempfile import shutil from pathlib import Path class SecureFileHandler: """Handle test files securely.""" def create_secure_temp_file(self, content, prefix="test_"): """Create temporary file with secure permissions.""" # Create file with restricted permissions fd, path = tempfile.mkstemp(prefix=prefix) try: # Write content with os.fdopen(fd, 'w') as f: f.write(content) # Set restrictive permissions (owner only) os.chmod(path, 0o600) return path except Exception as e: # Clean up on error os.unlink(path) raise def secure_cleanup(self, path): """Securely delete file.""" if Path(path).exists(): # Overwrite with random data before deletion with open(path, 'ba+', buffering=0) as f: length = f.tell() f.seek(0) f.write(os.urandom(length)) # Remove file os.unlink(path) ``` ## Network security ### TLS/SSL configuration ```yaml # mcpeval.yaml - Secure network settings network: # Enforce TLS require_tls: true min_tls_version: "1.2" # Certificate verification verify_certificates: true ca_bundle: "/path/to/ca-certificates.crt" # Client certificates client_cert: "/path/to/client.crt" client_key: "/path/to/client.key" # Timeouts connect_timeout: 30 read_timeout: 60 ``` ### Secure proxy configuration ```python # security/proxy.py import os class SecureProxy: """Configure secure proxy settings.""" @staticmethod def configure(): """Set up secure proxy configuration.""" proxy_url = os.environ.get('HTTPS_PROXY') if proxy_url: # Parse and validate proxy URL from urllib.parse import urlparse parsed = urlparse(proxy_url) # Ensure HTTPS proxy if parsed.scheme != 'https': raise ValueError("Only HTTPS proxies are allowed") # Set proxy with authentication if needed if parsed.username and parsed.password: # Don't log credentials! proxy_auth = f"{parsed.username}:***@" else: proxy_auth = "" safe_proxy = f"https://{proxy_auth}{parsed.hostname}:{parsed.port}" return { 'http': safe_proxy, 'https': safe_proxy, 'no_proxy': 'localhost,127.0.0.1' } return None ``` ### Network isolation ```yaml # docker-compose.security.yml version: '3.8' services: mcp-eval: image: mcp-eval:latest networks: - test-network security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE read_only: true tmpfs: - /tmp - /var/run networks: test-network: driver: bridge internal: true # No external access ipam: config: - subnet: 172.28.0.0/24 ``` ## Access control ### Role-based access ```python # security/rbac.py from enum import Enum from functools import wraps class Role(Enum): ADMIN = "admin" DEVELOPER = "developer" TESTER = "tester" VIEWER = "viewer" class Permissions(Enum): RUN_TESTS = "run_tests" VIEW_RESULTS = "view_results" MODIFY_CONFIG = "modify_config" ACCESS_SECRETS = "access_secrets" # Role permissions mapping ROLE_PERMISSIONS = { Role.ADMIN: [ Permissions.RUN_TESTS, Permissions.VIEW_RESULTS, Permissions.MODIFY_CONFIG, Permissions.ACCESS_SECRETS ], Role.DEVELOPER: [ Permissions.RUN_TESTS, Permissions.VIEW_RESULTS, Permissions.MODIFY_CONFIG ], Role.TESTER: [ Permissions.RUN_TESTS, Permissions.VIEW_RESULTS ], Role.VIEWER: [ Permissions.VIEW_RESULTS ] } def require_permission(permission): """Decorator to check permissions.""" def decorator(func): @wraps(func) def wrapper(user, *args, **kwargs): if not has_permission(user, permission): raise PermissionError( f"User {user.name} lacks {permission.value} permission" ) return func(user, *args, **kwargs) return wrapper return decorator def has_permission(user, permission): """Check if user has permission.""" user_permissions = ROLE_PERMISSIONS.get(user.role, []) return permission in user_permissions # Usage class User: def __init__(self, name, role): self.name = name self.role = role @require_permission(Permissions.RUN_TESTS) def run_tests(user, test_suite): """Run tests - requires RUN_TESTS permission.""" print(f"{user.name} running {test_suite}") ``` ### Authentication ```python # security/auth.py import jwt import datetime from passlib.hash import bcrypt class Authentication: """Handle user authentication.""" def __init__(self, secret_key): self.secret_key = secret_key def hash_password(self, password): """Hash password securely.""" return bcrypt.hash(password) def verify_password(self, password, hashed): """Verify password against hash.""" return bcrypt.verify(password, hashed) def generate_token(self, user_id, role, expires_hours=24): """Generate JWT token.""" payload = { 'user_id': user_id, 'role': role, 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=expires_hours) } return jwt.encode(payload, self.secret_key, algorithm='HS256') def verify_token(self, token): """Verify and decode JWT token.""" try: payload = jwt.decode( token, self.secret_key, algorithms=['HS256'] ) return payload except jwt.ExpiredSignatureError: raise ValueError("Token has expired") except jwt.InvalidTokenError: raise ValueError("Invalid token") ``` ## Compliance and auditing ### Audit logging ```python # security/audit.py import json import datetime from pathlib import Path class AuditLogger: """Log security-relevant events.""" def __init__(self, log_file="audit.log"): self.log_file = Path(log_file) # Ensure log file has restrictive permissions self.log_file.touch(mode=0o600) def log_event(self, event_type, user, details, status="success"): """Log an audit event.""" event = { "timestamp": datetime.datetime.utcnow().isoformat(), "event_type": event_type, "user": user, "status": status, "details": self._sanitize_details(details) } # Append to audit log with open(self.log_file, 'a') as f: f.write(json.dumps(event) + '\n') def _sanitize_details(self, details): """Remove sensitive data from details.""" # Remove any API keys or secrets sanitized = {} for key, value in details.items(): if 'key' in key.lower() or 'secret' in key.lower(): sanitized[key] = "[REDACTED]" else: sanitized[key] = value return sanitized def log_test_run(self, user, test_suite, results): """Log test execution.""" self.log_event( "test_run", user, { "test_suite": test_suite, "passed": results['passed'], "failed": results['failed'], "duration_ms": results['duration_ms'] } ) def log_config_change(self, user, config_path, changes): """Log configuration changes.""" self.log_event( "config_change", user, { "config_file": config_path, "changes": changes } ) def log_secret_access(self, user, secret_name, purpose): """Log access to secrets.""" self.log_event( "secret_access", user, { "secret_name": secret_name, "purpose": purpose } ) ``` ### Compliance checks ```python # security/compliance.py class ComplianceChecker: """Check compliance with security standards.""" def check_gdpr_compliance(self, test_data): """Verify GDPR compliance.""" issues = [] # Check for PII if self._contains_pii(test_data): issues.append("Test data contains PII - ensure consent") # Check data retention if not self._has_retention_policy(test_data): issues.append("No data retention policy defined") # Check encryption if not self._is_encrypted(test_data): issues.append("PII data not encrypted") return issues def check_sox_compliance(self, audit_logs): """Verify SOX compliance for financial data.""" issues = [] # Check audit trail completeness if not self._complete_audit_trail(audit_logs): issues.append("Incomplete audit trail") # Check access controls if not self._proper_access_controls(): issues.append("Insufficient access controls") return issues def check_hipaa_compliance(self, test_data): """Verify HIPAA compliance for health data.""" issues = [] # Check for PHI if self._contains_phi(test_data): # Verify encryption if not self._is_encrypted_hipaa(test_data): issues.append("PHI not properly encrypted") # Check access logs if not self._has_access_logs(): issues.append("PHI access not logged") return issues ``` ## Security testing ### Security-focused test cases ```python # tests/test_security.py from mcp_eval import task, Expect @task("Test SQL injection prevention") async def test_sql_injection(agent, session): """Verify protection against SQL injection.""" # Attempt SQL injection malicious_input = "'; DROP TABLE users; --" response = await agent.generate_str( f"Search for user: {malicious_input}" ) # Should handle safely await session.assert_that( Expect.judge.llm( "Response shows input was sanitized and no SQL was executed", min_score=0.9 ), response=response ) # Verify no actual damage await session.assert_that( Expect.tools.failed("database"), # DB operation should fail safely name="injection_prevented" ) @task("Test XSS prevention") async def test_xss_prevention(agent, session): """Verify protection against XSS attacks.""" xss_payload = "" response = await agent.generate_str( f"Display message: {xss_payload}" ) # Should escape or sanitize await session.assert_that( Expect.content.not_contains("