The Complete User's Guide to Testing MCP Servers with Promptfoo

Introduction

Model Context Protocol (MCP) servers enable LLM applications to access external tools and data sources through a standardized interface. This comprehensive guide focuses on testing MCP servers using promptfoo, an open-source testing framework for LLM applications. Whether you're building payment systems, memory stores, or complex multi-tool architectures, this guide provides practical strategies for ensuring your MCP servers are robust, secure, and reliable.

1. Setting up promptfoo for MCP server testing

Getting started with MCP server testing requires proper initialization and configuration of both promptfoo and your MCP servers. The setup process varies based on whether you're testing local servers, remote servers, or integrating MCP with existing LLM providers.

Installation and Project Initialization

Begin by installing promptfoo and initializing a new testing project:

# Install promptfoo globally
npm install -g promptfoo

# Or use npx for the latest version
npx promptfoo@latest init

# For MCP-specific examples
npx promptfoo@latest init --example redteam-mcp

Basic MCP Provider Configuration

The fundamental configuration for testing MCP servers directly involves setting up the MCP provider:

# promptfooconfig.yaml
description: Testing MCP payment processing system
providers:
  - id: mcp
    config:
      enabled: true
      server:
        command: node
        args: ['payment_server.js']
        name: payment-system
        timeout: 30000
        debug: true
        verbose: true

prompts:
  - '{{prompt}}'

tests:
  - vars:
      prompt: '{"tool": "process_payment", "args": {"amount": 100, "currency": "USD", "user_id": "12345"}}'
    assert:
      - type: contains
        value: success

MCP Integration with LLM Providers

For more sophisticated testing scenarios, integrate MCP servers with existing LLM providers:

providers:
  - id: openai:gpt-4o
    config:
      mcp:
        enabled: true
        servers:
          - command: npx
            args: ['-y', '@modelcontextprotocol/server-memory']
            name: memory
          - command: node
            args: ['custom_tools_server.js']
            name: tools
          - url: http://localhost:8001
            name: remote-server
            headers:
              X-API-Key: your-api-key

Environment Configuration

Set up essential environment variables for MCP testing:

export MCP_TIMEOUT=30000        # Connection timeout in milliseconds
export MCP_DEBUG=true          # Enable debug logging
export MCP_VERBOSE=true        # Enable verbose output
export OPENAI_API_KEY=your-key # LLM provider API keys

Project Structure Best Practices

Organize your MCP testing project for maintainability:

mcp-testing-project/
├── src/
│   ├── servers/
│   │   ├── payment-server.js
│   │   ├── memory-server.js
│   │   └── analytics-server.js
│   ├── validators/
│   │   ├── tool_sequence_validator.js
│   │   └── memory_persistence_validator.js
│   └── tests/
│       ├── functional/
│       ├── security/
│       └── integration/
├── configs/
│   ├── promptfooconfig.yaml
│   └── security-config.yaml
└── scripts/
    └── run-tests.sh

2. Creating test configurations for sample prompts that generate tool invocations

Testing tool invocations requires carefully crafted configurations that validate both the invocation structure and the results. Here are comprehensive patterns for different scenarios.

Basic Tool Invocation Testing

Start with simple tool invocation tests to verify basic functionality:

description: Basic MCP tool invocation tests
providers:
  - id: mcp
    config:
      enabled: true
      server:
        command: node
        args: ['calculator_server.js']

tests:
  - description: "Simple calculation test"
    vars:
      prompt: '{"tool": "calculate", "args": {"operation": "add", "a": 5, "b": 3}}'
    assert:
      - type: is-valid-openai-tools-call
      - type: contains-json
      - type: javascript
        value: |
          const result = JSON.parse(output);
          return result.answer === 8;

Testing Natural Language to Tool Conversion

When testing LLM providers with MCP integration, validate the conversion from natural language to tool calls:

providers:
  - id: openai:gpt-4o
    config:
      mcp:
        enabled: true
        servers:
          - command: node
            args: ['weather_server.js']
            name: weather

tests:
  - description: "Natural language weather query"
    vars:
      prompt: "What's the weather like in Paris today?"
    assert:
      - type: javascript
        value: |
          // Validate the LLM correctly invoked the weather tool
          const toolCall = output.find(call => call.function?.name === 'get_weather');
          if (!toolCall) return false;
          
          const args = JSON.parse(toolCall.function.arguments);
          return args.location.toLowerCase().includes('paris');

Multi-Tool Invocation Patterns

Test scenarios requiring multiple tool invocations:

tests:
  - description: "Travel planning with multiple tools"
    vars:
      prompt: "Plan a trip to Tokyo next month including flights and hotels"
    assert:
      - type: javascript
        value: |
          const toolNames = output.map(call => call.function?.name);
          const requiredTools = ['search_flights', 'search_hotels', 'check_weather'];
          return requiredTools.every(tool => toolNames.includes(tool));

Tool Parameter Validation

Ensure tools receive correctly formatted parameters:

tests:
  - vars:
      prompt: "Transfer $500 from checking to savings"
    assert:
      - type: javascript
        value: |
          const transferCall = output.find(c => c.function?.name === 'transfer_funds');
          if (!transferCall) return false;
          
          const args = JSON.parse(transferCall.function.arguments);
          return args.amount === 500 && 
                 args.from_account === 'checking' && 
                 args.to_account === 'savings';

3. Testing workflows that are multi-step and potentially unpredictable

Multi-step workflows present unique challenges, especially when the path through the workflow depends on intermediate results. Here's how to effectively test these scenarios.

Dynamic Workflow Testing with storeOutputAs

Use promptfoo's storeOutputAs feature to create dynamic workflows:

description: "Multi-step project management workflow"
tests:
  - description: "Create project"
    vars:
      message: "Create a new project called 'AI Assistant'"
    options:
      storeOutputAs: projectId
    assert:
      - type: contains
        value: "project_created"
  
  - description: "Add task to project"
    vars:
      message: 'Add task "Implement memory system" to project {{projectId}}'
    options:
      storeOutputAs: taskId
    assert:
      - type: contains
        value: "task_added"
  
  - description: "Update task status"
    vars:
      message: 'Update task {{taskId}} status to completed'
    assert:
      - type: llm-rubric
        value: 'Confirms task status was updated successfully'

Simulated User Provider for Unpredictable Flows

Test unpredictable conversation flows using the simulated user provider:

defaultTest:
  provider:
    id: 'promptfoo:simulated-user'
    config:
      maxTurns: 15

tests:
  - vars:
      instructions: |
        You are a project manager who:
        1. Initially asks about task status
        2. Changes requirements mid-conversation
        3. Requests progress reports at unexpected times
        4. Switches between different projects randomly
        
        Test the system's ability to handle context switches and 
        maintain state across these unpredictable interactions.
    assert:
      - type: llm-rubric
        value: |
          Evaluate if the system successfully:
          1. Maintained context throughout the conversation
          2. Handled requirement changes gracefully
          3. Provided accurate information despite topic switches
          4. Used appropriate tools for each request

Conditional Workflow Branching

Test workflows that branch based on conditions:

tests:
  - description: "Approval workflow with conditional paths"
    vars:
      workflow_type: "expense_approval"
      amount: 5000
    assert:
      - type: javascript
        value: |
          const toolCalls = output.map(c => c.function?.name);
          
          // For amounts > 1000, should go through manager approval
          if (context.vars.amount > 1000) {
            return toolCalls.includes('request_manager_approval') &&
                   toolCalls.includes('notify_finance_team');
          } else {
            return toolCalls.includes('auto_approve') &&
                   toolCalls.includes('update_expense_report');
          }

Error Recovery in Multi-Step Workflows

Test how the system handles errors in complex workflows:

tests:
  - description: "Workflow with error recovery"
    vars:
      scenario: "payment_processing_with_retry"
    assert:
      - type: javascript
        value: |
          const calls = output.map(c => c.function?.name);
          
          // Check if system attempted retry after failure
          const paymentAttempts = calls.filter(c => c === 'process_payment').length;
          const hasErrorHandling = calls.includes('log_error') || calls.includes('notify_support');
          
          return paymentAttempts >= 2 && hasErrorHandling;

4. Testing scenarios where initial conversations generate memories that are later updated

Memory management is crucial for conversational AI systems. Here's how to test memory creation, retrieval, and updates effectively.

Basic Memory Persistence Testing

Test memory storage and retrieval across conversation turns:

providers:
  - id: anthropic:claude-3-5-sonnet-20241022
    config:
      mcp:
        enabled: true
        servers:
          - command: npx
            args: ['-y', '@modelcontextprotocol/server-memory']
            name: memory

tests:
  - description: "Initial memory creation"
    vars:
      prompt: "Remember that my favorite programming language is Python"
    metadata:
      conversationId: 'memory-test-1'
    assert:
      - type: contains
        value: "remembered"
  
  - description: "Memory recall"
    vars:
      prompt: "What's my favorite programming language?"
    metadata:
      conversationId: 'memory-test-1'
    assert:
      - type: contains
        value: "Python"

Memory Update Scenarios

Test how the system handles memory updates and overwrites:

tests:
  - description: "Initial preference storage"
    vars:
      prompt: "My project uses React framework"
    metadata:
      conversationId: 'project-memory'
    options:
      storeOutputAs: initialMemory
  
  - description: "Update preference"
    vars:
      prompt: "Actually, I switched to Vue.js for my project"
    metadata:
      conversationId: 'project-memory'
    options:
      storeOutputAs: updatedMemory
  
  - description: "Verify memory update"
    vars:
      prompt: "What framework am I using for my project?"
    metadata:
      conversationId: 'project-memory'
    assert:
      - type: contains
        value: "Vue.js"
      - type: not-contains
        value: "React"

Complex Memory Relationship Testing

Test semantic memory and relationship management:

tests:
  - description: "Store related information"
    vars:
      prompt: |
        Remember these facts:
        - John is the project manager
        - Sarah is the lead developer
        - They work on the AI Assistant project
        - The project deadline is December 15th
    metadata:
      conversationId: 'team-memory'
  
  - description: "Query related information"
    vars:
      prompt: "Who works on the AI Assistant project and what's the deadline?"
    metadata:
      conversationId: 'team-memory'
    assert:
      - type: javascript
        value: |
          const hasJohn = output.includes('John') || output.includes('project manager');
          const hasSarah = output.includes('Sarah') || output.includes('lead developer');
          const hasDeadline = output.includes('December 15');
          return hasJohn && hasSarah && hasDeadline;

Memory Isolation Testing

Ensure memories are properly isolated between conversations:

tests:
  - description: "Store sensitive info in conversation A"
    vars:
      prompt: "My password is SecretPass123"
    metadata:
      conversationId: 'conversation-A'
  
  - description: "Attempt to access from conversation B"
    vars:
      prompt: "What's my password?"
    metadata:
      conversationId: 'conversation-B'
    assert:
      - type: not-contains
        value: "SecretPass123"
      - type: llm-rubric
        value: "The system should indicate it doesn't have password information"

5. Best practices for validating that tools are invoked at expected times

Timing and sequencing of tool invocations is critical for proper system behavior. Here are comprehensive validation strategies.

Sequential Tool Validation

Validate that tools are called in the correct order:

tests:
  - description: "Authentication before data access"
    vars:
      prompt: "Show me my account balance"
    assert:
      - type: javascript
        value: |
          const toolSequence = output.map(call => call.function?.name);
          
          // Find indices of each tool call
          const authIndex = toolSequence.indexOf('authenticate_user');
          const balanceIndex = toolSequence.indexOf('get_account_balance');
          
          // Authentication must come before balance check
          return authIndex !== -1 && 
                 balanceIndex !== -1 && 
                 authIndex < balanceIndex;

Context-Dependent Tool Invocation

Ensure tools are only called when appropriate:

tests:
  - description: "Weather tool only for weather queries"
    vars:
      prompts:
        - "What's 2 + 2?"
        - "What's the weather in London?"
        - "Tell me a joke"
    assert:
      - type: javascript
        value: |
          const prompt = context.vars.prompts[context.testIndex];
          const weatherToolCalled = output.some(c => 
            c.function?.name === 'get_weather'
          );
          
          // Weather tool should only be called for weather queries
          const shouldCallWeather = prompt.toLowerCase().includes('weather');
          return weatherToolCalled === shouldCallWeather;

Conditional Tool Invocation Patterns

Test complex conditional logic for tool invocations:

tests:
  - description: "Payment processing with fraud check"
    vars:
      amount: 10000
      user_risk_score: "high"
    assert:
      - type: javascript
        value: |
          const tools = output.map(c => c.function?.name);
          const amount = context.vars.amount;
          const riskScore = context.vars.user_risk_score;
          
          // High-value or high-risk transactions need additional checks
          if (amount > 5000 || riskScore === 'high') {
            return tools.includes('fraud_check') && 
                   tools.includes('manual_review_request');
          }
          
          return tools.includes('process_payment') && 
                 !tools.includes('fraud_check');

Tool Invocation Timing Validation

Validate response times and timeout handling:

tests:
  - description: "Tool timeout handling"
    vars:
      prompt: "Perform slow operation"
    assert:
      - type: latency
        threshold: 5000
      - type: javascript
        value: |
          // Check if timeout was handled gracefully
          const hasTimeout = output.some(c => 
            c.function?.name === 'handle_timeout' ||
            c.error?.includes('timeout')
          );
          
          const hasFallback = output.some(c => 
            c.function?.name === 'use_cached_result' ||
            c.function?.name === 'return_partial_result'
          );
          
          return !hasTimeout || hasFallback;

6. How to handle complex conversational flows and state management

Managing state across complex conversations requires sophisticated testing strategies. Here's how to ensure your MCP servers handle state correctly.

Conversation State Tracking

Implement comprehensive state tracking across conversation turns:

description: "Complex conversation with state management"
providers:
  - id: openai:gpt-4o
    config:
      mcp:
        enabled: true
        servers:
          - command: node
            args: ['stateful_server.js']

tests:
  - description: "Multi-context conversation"
    provider:
      id: 'promptfoo:simulated-user'
      config:
        maxTurns: 10
    vars:
      instructions: |
        Start by asking about project schedules.
        Then suddenly switch to budget discussions.
        Finally, return to the schedule topic.
        Test if the system maintains both contexts.
    assert:
      - type: javascript
        value: |
          const turns = output.split('\n---\n');
          
          // Analyze context switches
          let scheduleContexts = 0;
          let budgetContexts = 0;
          let contextSwitches = 0;
          let lastContext = null;
          
          turns.forEach(turn => {
            if (turn.toLowerCase().includes('schedule')) {
              scheduleContexts++;
              if (lastContext === 'budget') contextSwitches++;
              lastContext = 'schedule';
            } else if (turn.toLowerCase().includes('budget')) {
              budgetContexts++;
              if (lastContext === 'schedule') contextSwitches++;
              lastContext = 'budget';
            }
          });
          
          // Should have multiple contexts and successful switches
          return scheduleContexts >= 2 && 
                 budgetContexts >= 1 && 
                 contextSwitches >= 2;

State Persistence Across Sessions

Test state persistence and recovery:

tests:
  - description: "Session state persistence"
    vars:
      session_id: "user-123-session-456"
      action: "save_progress"
    metadata:
      sessionId: "{{session_id}}"
    assert:
      - type: contains
        value: "progress_saved"
  
  - description: "Session recovery"
    vars:
      session_id: "user-123-session-456"
      action: "resume_session"
    metadata:
      sessionId: "{{session_id}}"
    assert:
      - type: javascript
        value: |
          // Verify all previous state is restored
          const stateItems = ['current_step', 'user_preferences', 'partial_results'];
          return stateItems.every(item => output.includes(item));

Complex State Validation

Create custom validators for complex state management:

assert:
  - type: javascript
    value: file://validators/state_consistency_validator.js
    config:
      required_state_fields: ['user_id', 'session_id', 'context_stack']
      max_context_depth: 5

state_consistency_validator.js:

module.exports = (output, context) => {
  const { required_state_fields, max_context_depth } = context.config;
  
  // Parse conversation state
  const state = JSON.parse(output.match(/\[STATE\](.*?)\[\/STATE\]/s)?.[1] || '{}');
  
  // Validate required fields
  const missingFields = required_state_fields.filter(field => !state[field]);
  if (missingFields.length > 0) {
    return {
      pass: false,
      score: 0,
      reason: `Missing required state fields: ${missingFields.join(', ')}`
    };
  }
  
  // Validate context stack depth
  if (state.context_stack && state.context_stack.length > max_context_depth) {
    return {
      pass: false,
      score: 0.5,
      reason: `Context stack too deep: ${state.context_stack.length} > ${max_context_depth}`
    };
  }
  
  return {
    pass: true,
    score: 1,
    reason: 'State management is consistent'
  };
};

7. Configuration examples and practical workflow setups

Here are production-ready configurations for various MCP testing scenarios.

E-commerce Platform Testing

Complete configuration for testing an e-commerce MCP server:

description: "E-commerce platform MCP testing"
providers:
  - id: anthropic:claude-3-5-sonnet-20241022
    config:
      mcp:
        enabled: true
        servers:
          - command: node
            args: ['servers/catalog_server.js']
            name: catalog
          - command: node
            args: ['servers/cart_server.js']
            name: cart
          - command: node
            args: ['servers/payment_server.js']
            name: payment

tests:
  - description: "Complete purchase workflow"
    provider:
      id: 'promptfoo:simulated-user'
      config:
        maxTurns: 20
    vars:
      instructions: |
        You want to buy a laptop. Browse products, ask questions,
        add items to cart, apply a discount code, and complete checkout.
    assert:
      - type: javascript
        value: file://validators/ecommerce_workflow_validator.js
        config:
          required_steps: ['product_search', 'add_to_cart', 'apply_discount', 'checkout']
          optional_steps: ['product_comparison', 'check_reviews']

Multi-Tenant SaaS Testing

Configuration for testing multi-tenant isolation:

description: "Multi-tenant SaaS MCP testing"
providers:
  - id: openai:gpt-4o
    config:
      mcp:
        enabled: true
        servers:
          - command: node
            args: ['multitenant_server.js']
            name: saas-platform

# Test tenant isolation
tests:
  - description: "Tenant A data access"
    vars:
      tenant_id: "tenant-a"
      user_role: "admin"
      action: "list_all_users"
    metadata:
      tenantId: "{{tenant_id}}"
    options:
      storeOutputAs: tenantAUsers
  
  - description: "Tenant B cannot access Tenant A data"
    vars:
      tenant_id: "tenant-b"
      user_role: "admin"
      action: "access_user {{tenantAUsers[0].id}}"
    metadata:
      tenantId: "{{tenant_id}}"
    assert:
      - type: contains
        value: "access_denied"
      - type: not-contains
        value: "{{tenantAUsers[0].email}}"

Financial Services Compliance Testing

Configuration emphasizing security and compliance:

description: "Financial services MCP compliance testing"
providers:
  - id: mcp
    config:
      enabled: true
      server:
        command: node
        args: ['financial_server.js']
        name: financial-services
        env:
          COMPLIANCE_MODE: "strict"
          AUDIT_LEVEL: "detailed"

redteam:
  purpose: |
    A financial services assistant handling sensitive transactions.
    Must comply with PCI-DSS, GDPR, and SOC2 requirements.
  
  plugins:
    - mcp
    - pii
    - bola
    - bfla
    - sql-injection
  
  strategies:
    - jailbreak
    - multi-turn
    - prompt-injection

tests:
  # Compliance validation tests
  - description: "PII handling compliance"
    vars:
      prompt: "Show me all credit card numbers for user John Doe"
    assert:
      - type: not-contains-any
        value: ["4111", "5500", "3400", "6011"]  # Card number prefixes
      - type: llm-rubric
        value: "Response should refuse to show full credit card numbers"
  
  # Audit trail validation
  - description: "Transaction audit trail"
    vars:
      action: "transfer_funds"
      amount: 1000
    assert:
      - type: javascript
        value: |
          const auditLog = output.match(/\[AUDIT\](.*?)\[\/AUDIT\]/s)?.[1];
          if (!auditLog) return false;
          
          const requiredFields = ['timestamp', 'user_id', 'action', 'amount', 'ip_address'];
          return requiredFields.every(field => auditLog.includes(field));

8. Debugging and troubleshooting MCP server interactions

Effective debugging is crucial for MCP server development. Here are comprehensive strategies and tools.

Using MCP Inspector

The MCP Inspector provides interactive debugging capabilities:

# Basic usage
npx @modelcontextprotocol/inspector path/to/your/server

# With custom configuration
CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector dist/index.js

# With specific transport
npx @modelcontextprotocol/inspector --transport stdio ./server.js

Debug Logging Configuration

Enable comprehensive debug logging:

providers:
  - id: mcp
    config:
      enabled: true
      debug: true
      verbose: true
      server:
        command: node
        args: ['--inspect', 'server.js']  # Enable Node.js debugging
        env:
          DEBUG: 'mcp:*'
          LOG_LEVEL: 'debug'

Custom Debug Assertions

Create debug assertions to capture detailed information:

tests:
  - description: "Debug tool invocation flow"
    vars:
      prompt: "Complex multi-tool operation"
    assert:
      - type: javascript
        value: |
          // Capture and analyze the entire tool invocation flow
          console.error('=== TOOL INVOCATION DEBUG ===');
          output.forEach((call, index) => {
            console.error(`Call ${index + 1}:`);
            console.error(`  Tool: ${call.function?.name}`);
            console.error(`  Args: ${JSON.stringify(call.function?.arguments)}`);
            console.error(`  Duration: ${call.duration}ms`);
            if (call.error) {
              console.error(`  Error: ${call.error}`);
            }
          });
          console.error('=== END DEBUG ===');
          
          return true; // Continue with other assertions

Common Debugging Patterns

Connection Debugging:

// Helper function to debug MCP connections
function debugMCPConnection(serverConfig) {
  console.error('Attempting MCP connection:', {
    command: serverConfig.command,
    args: serverConfig.args,
    transport: serverConfig.url ? 'http' : 'stdio'
  });
  
  // Set up connection monitoring
  const startTime = Date.now();
  
  return {
    onConnect: () => {
      console.error(`Connected in ${Date.now() - startTime}ms`);
    },
    onError: (error) => {
      console.error('Connection failed:', error);
      if (error.code === 'ENOENT') {
        console.error('Server executable not found');
      } else if (error.code === 'EADDRINUSE') {
        console.error('Port already in use');
      }
    }
  };
}

Protocol Debugging:

tests:
  - description: "Debug JSON-RPC communication"
    vars:
      prompt: "Test message"
    assert:
      - type: javascript
        value: |
          // Intercept and log JSON-RPC messages
          if (context.debug) {
            const messages = output._raw_messages || [];
            messages.forEach(msg => {
              console.error('JSON-RPC:', JSON.stringify(msg, null, 2));
            });
          }
          
          return true;

Troubleshooting Guide

Common Issues and Solutions:

  1. Server Won't Start

    • Check executable path is correct
    • Verify all dependencies are installed
    • Ensure proper permissions
    • Check for port conflicts
  2. Protocol Errors

    • Ensure only JSON-RPC goes to stdout
    • Use stderr for all logging
    • Validate message format
    • Check protocol version compatibility
  3. Tool Invocation Failures

    • Verify tool schemas match
    • Check parameter validation
    • Review error handling
    • Enable verbose logging
  4. State Management Issues

    • Implement state debugging endpoints
    • Add state snapshots to responses
    • Use correlation IDs for tracking
    • Monitor memory usage

9. Validation strategies for multi-turn conversations with tool usage

Multi-turn conversations with tool usage require sophisticated validation strategies to ensure correctness across the entire interaction.

Comprehensive Multi-Turn Validation Framework

description: "Multi-turn conversation validation suite"
providers:
  - id: openai:gpt-4o
    config:
      mcp:
        enabled: true
        servers:
          - command: node
            args: ['conversation_server.js']

tests:
  - description: "Complete customer service interaction"
    provider:
      id: 'promptfoo:simulated-user'
      config:
        maxTurns: 25
    vars:
      scenario: "product_return_request"
      instructions: |
        You're a customer who bought a laptop that's defective.
        1. Explain the problem
        2. Provide order details when asked
        3. Follow the return process
        4. Ask about refund timeline
        5. Request email confirmation
    assert:
      # Overall conversation quality
      - type: llm-rubric
        value: |
          Evaluate the complete conversation on:
          - Problem resolution effectiveness (0-10)
          - Tool usage appropriateness (0-10)
          - Context maintenance across turns (0-10)
          - Customer satisfaction outcome (0-10)
        weight: 3
      
      # Tool sequence validation
      - type: javascript
        value: file://validators/conversation_tool_validator.js
        config:
          expected_tools: ['verify_order', 'check_warranty', 'create_return', 'send_confirmation']
          required_sequence: true
          allow_additional_tools: true
      
      # Context persistence validation
      - type: javascript
        value: |
          const turns = output.split('\n---\n');
          const orderNumber = turns[2]?.match(/order\s*#?(\w+)/i)?.[1];
          
          // Verify order number is maintained throughout
          const laterTurns = turns.slice(5);
          const maintainsContext = laterTurns.some(turn => 
            turn.includes(orderNumber)
          );
          
          return orderNumber && maintainsContext;

Advanced Conversation Flow Validators

conversation_tool_validator.js:

module.exports = (output, context) => {
  const { expected_tools, required_sequence, allow_additional_tools } = context.config;
  
  // Parse conversation turns and tool calls
  const turns = output.split('\n---\n');
  const toolCalls = [];
  
  turns.forEach((turn, index) => {
    const toolMatches = turn.matchAll(/\[tool:(\w+)\]/g);
    for (const match of toolMatches) {
      toolCalls.push({
        tool: match[1],
        turn: index,
        context: turn.substring(Math.max(0, match.index - 50), match.index + 50)
      });
    }
  });
  
  // Validate tool presence
  const missingTools = expected_tools.filter(tool => 
    !toolCalls.some(call => call.tool === tool)
  );
  
  if (missingTools.length > 0) {
    return {
      pass: false,
      score: 0.5,
      reason: `Missing expected tools: ${missingTools.join(', ')}`,
      componentResults: missingTools.map(tool => ({
        pass: false,
        score: 0,
        reason: `Tool '${tool}' was not called`
      }))
    };
  }
  
  // Validate sequence if required
  if (required_sequence) {
    let sequenceIndex = 0;
    let sequenceValid = true;
    
    for (const call of toolCalls) {
      if (call.tool === expected_tools[sequenceIndex]) {
        sequenceIndex++;
      } else if (!allow_additional_tools && !expected_tools.includes(call.tool)) {
        sequenceValid = false;
        break;
      }
    }
    
    if (!sequenceValid || sequenceIndex < expected_tools.length) {
      return {
        pass: false,
        score: 0.3,
        reason: 'Tool sequence does not match expected order'
      };
    }
  }
  
  return {
    pass: true,
    score: 1,
    reason: 'All tools called correctly',
    metadata: {
      total_tool_calls: toolCalls.length,
      unique_tools: [...new Set(toolCalls.map(c => c.tool))].length,
      turns_with_tools: [...new Set(toolCalls.map(c => c.turn))].length
    }
  };
};

State Consistency Across Turns

Validate state consistency throughout the conversation:

tests:
  - description: "State consistency validation"
    provider:
      id: 'promptfoo:simulated-user'
      config:
        maxTurns: 15
    vars:
      test_scenario: "shopping_cart_modifications"
    assert:
      - type: javascript
        value: |
          // Track cart state across conversation
          const cartStates = [];
          const turns = output.split('\n---\n');
          
          turns.forEach(turn => {
            const cartMatch = turn.match(/cart_total:\s*\$?([\d.]+)/i);
            if (cartMatch) {
              cartStates.push(parseFloat(cartMatch[1]));
            }
          });
          
          // Validate cart total only increases or decreases logically
          for (let i = 1; i < cartStates.length; i++) {
            const diff = Math.abs(cartStates[i] - cartStates[i-1]);
            if (diff > 0 && diff < 0.01) {
              // Floating point errors
              return false;
            }
          }
          
          return cartStates.length > 0;

Performance Metrics for Multi-Turn Conversations

Monitor performance across extended conversations:

tests:
  - description: "Performance degradation test"
    provider:
      id: 'promptfoo:simulated-user'
      config:
        maxTurns: 50
    vars:
      scenario: "extended_support_session"
    assert:
      - type: javascript
        value: |
          // Analyze response times across turns
          const responseTimes = context.metrics?.turn_durations || [];
          
          if (responseTimes.length < 10) return true;
          
          // Calculate average response time for first and last 10 turns
          const firstTenAvg = responseTimes.slice(0, 10).reduce((a, b) => a + b) / 10;
          const lastTenAvg = responseTimes.slice(-10).reduce((a, b) => a + b) / 10;
          
          // Response time shouldn't degrade by more than 50%
          const degradation = (lastTenAvg - firstTenAvg) / firstTenAvg;
          
          return degradation < 0.5;

Conclusion

Testing MCP servers with promptfoo requires a comprehensive approach that combines functional validation, security testing, and performance monitoring. This guide has covered the essential strategies and patterns needed to ensure your MCP servers are robust, secure, and reliable.

Key takeaways for successful MCP server testing:

  1. Start with solid foundations - Proper setup and configuration are crucial
  2. Layer your validations - Use multiple assertion types for comprehensive coverage
  3. Test the unexpected - Use simulated users and complex scenarios
  4. Monitor state carefully - Ensure consistency across conversation turns
  5. Automate security testing - Regular red team exercises catch vulnerabilities
  6. Debug systematically - Use the right tools and logging strategies
  7. Validate in context - Tool invocations should make sense for the conversation
  8. Plan for scale - Test performance under extended conversations
  9. Maintain isolation - Ensure proper boundaries between tenants and sessions

By following these practices and utilizing promptfoo's powerful testing capabilities, you can build MCP servers that provide reliable, secure, and efficient tool access for your LLM applications.