Production MCP Systems

Testing and Best Practices

5 min read

There is a category of bug here that ordinary tests cannot see: your server can be entirely correct and still be unusable, because the model never calls the tool or calls it with the wrong arguments. Each test level below catches a different failure, and the last one is the one people skip.

Test Categories

Three levels, three different failures

Fast

Unit

TestsOne function in isolation
CatchesWrong logic, bad validation
SpeedMilliseconds — run on save
MissesAnything about how pieces connect
Pros
  • Cheap enough to run constantly
  • Failures point at one function
Cons
  • A server can pass every unit test and still not start
Protocol-level

Integration

TestsReal requests through the real dispatcher
CatchesBroken wiring, malformed schemas, bad envelopes
SpeedSeconds — run in CI
MissesWhether a model would choose this tool
Pros
  • Verifies tools/list and tools/call actually work
  • Catches the registration mistakes unit tests cannot
Cons
  • Needs fixtures and mocked upstreams to stay fast
Most skipped

End-to-end with a real model

TestsA real host, a real model, a real prompt
CatchesDescriptions the model misreads or ignores
SpeedSlow and non-deterministic
MissesLittle — but it won't tell you why
Pros
  • The only level that tests your tool descriptions at all
  • Surfaces tools that are never selected, which no other test can
Cons
  • Non-deterministic, so treat failures as signal rather than gates
  • Costs a real API call per run

The third column is the one that matters most and gets automated least. Your tool descriptions are, functionally, part of your interface — and unit tests cannot read them. A short checklist of real prompts, run by hand before each release, catches more real problems than another dozen unit tests: does the model reach for the right tool, with sensible arguments, without being told the tool's name?

Unit Testing Tools

import pytest
from unittest.mock import AsyncMock, patch

from server import WeatherTool

class TestWeatherTool:
    def test_get_definition(self):
        tool = WeatherTool()
        definition = tool.get_tool_definition()

        assert definition["name"] == "get_weather"
        assert "city" in definition["inputSchema"]["properties"]

    def test_validate_valid_input(self):
        tool = WeatherTool()
        assert tool.validate({"city": "London"}) == True

    def test_validate_empty_city(self):
        tool = WeatherTool()
        with pytest.raises(ValueError, match="City is required"):
            tool.validate({"city": ""})

    @pytest.mark.asyncio
    async def test_execute(self):
        tool = WeatherTool()
        result = await tool.execute({"city": "London"})

        assert "temperature" in result
        assert "condition" in result

Integration Testing

import pytest
from httpx import AsyncClient
from server import app

@pytest.fixture
async def client():
    async with AsyncClient(app=app, base_url="http://test") as ac:
        yield ac

@pytest.mark.asyncio
async def test_mcp_endpoint(client):
    response = await client.post("/mcp", json={
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list"
    })

    assert response.status_code == 200
    data = response.json()
    assert "tools" in data

@pytest.mark.asyncio
async def test_tool_call(client):
    response = await client.post("/mcp", json={
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {
            "name": "get_weather",
            "arguments": {"city": "Tokyo"}
        }
    })

    assert response.status_code == 200
    assert response.json()["result"]

Mocking External Services

@pytest.mark.asyncio
async def test_weather_api_failure():
    with patch("server.fetch_weather", new_callable=AsyncMock) as mock:
        mock.side_effect = Exception("API unavailable")

        tool = WeatherTool()
        with pytest.raises(McpError) as exc:
            await tool.execute({"city": "London"})

        assert "unavailable" in str(exc.value)

Best Practices Summary

PracticeBenefit
Use environment variablesSecurity, flexibility
Implement health checksReliability monitoring
Add request timeoutsPrevent hanging
Log structured dataEasy debugging
Version your APIBackwards compatibility
Document tools clearlyBetter AI usage
Test edge casesRobust handling

Production Checklist

  • All secrets in environment variables
  • Health check endpoint implemented
  • Prometheus metrics exposed
  • Structured logging configured
  • Rate limiting enabled
  • CORS properly configured
  • SSL/TLS enabled
  • Error handling comprehensive
  • Tests passing in CI/CD

Next: the capstone — putting all five modules into one server you actually run. :::

Quiz

Module 5 Quiz: Production MCP Systems

Take Quiz
Was this lesson helpful?

Sign in to rate