MCP Servers Explained: How the Model Context Protocol Works

Claude Certification Guide11 min read
Tool Design & MCP Integration

MCP is a protocol that lets Claude talk to external tools and data sources. Databases, APIs, file systems, third-party services. You build one MCP server, and any compatible client can connect to it. No more writing custom integration code for every service you want the model to reach.

If you're using the API, Claude Code, or the desktop app, MCP is how you give Claude capabilities beyond text generation. What follows covers the architecture, a working server implementation, and the specific patterns the Claude Certified Architect exam likes to test.

The architecture

MCP uses a client-server model built around three primitives:

Resources are data the server exposes for reading. Think GET endpoints: a database schema, a file's contents, a config object. Read-only.

Tools are functions the model can call. These do things: query a database, create a file, send a message. Each tool has an input schema and returns structured results.

Prompts are reusable templates the server provides. You'll see these less often than resources and tools, but they're handy for standardising how the model talks to a particular domain.

Then there's the transport layer. MCP supports two options:

  • stdio runs the server as a subprocess, communicating over stdin/stdout. This is how Claude Code and the desktop app run local servers.
  • HTTP with SSE runs the server as a web service. Requests go over HTTP, responses stream back via Server-Sent Events. You'd use this for remote deployments.

Building an MCP server

Here's a minimal Python server using FastMCP with one tool and one resource:

python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("demo-server")

@mcp.resource("config://app")
def get_config() -> str:
    """Return the application configuration."""
    return '{"version": "1.2.0", "environment": "production"}'

@mcp.tool()
def calculate_discount(price: float, percentage: float) -> float:
    """Calculate a discounted price.

    Args:
        price: The original price in GBP.
        percentage: The discount percentage (0-100).

    Returns:
        The price after discount.
    """
    if not 0 <= percentage <= 100:
        raise ValueError("Percentage must be between 0 and 100")
    return round(price * (1 - percentage / 100), 2)

if __name__ == "__main__":
    mcp.run(transport="stdio")

Pay attention to the tool definition. Two things matter here:

  1. The description does the heavy lifting. Claude reads it to decide whether to call this tool. Something vague like "calculates things" would cause misrouting. "Calculate a discounted price" plus typed parameter descriptions gives the model what it needs.

  2. Validation lives inside the tool. The server checks that percentage falls within 0-100 and raises a clear error otherwise. Claude receives that error and can course-correct.

Clear descriptions, validated inputs, structured outputs. That's the pattern, and it's what the exam tests in Task 2.1 — Tool Schema Design.

Tool descriptions drive tool selection

This is the single most-tested MCP concept on the exam. And the underlying problem is straightforward: if two tools have vague, overlapping descriptions, Claude can't reliably pick the right one.

Bad descriptions (cause misrouting):

python
@mcp.tool()
def get_customer(identifier: str) -> dict:
    """Retrieves customer information."""
    ...

@mcp.tool()
def lookup_order(identifier: str) -> dict:
    """Retrieves order details."""
    ...

A user asks "check my order #12345" and Claude calls get_customer. Why? Both descriptions say roughly the same thing. The model's guessing.

Good descriptions (reliable routing):

python
@mcp.tool()
def get_customer(customer_id: str) -> dict:
    """Look up a customer profile by customer ID.

    Use this tool when the user asks about their account details,
    contact information, subscription status, or loyalty points.

    Input: A customer ID in the format CUST-XXXXX.
    Do NOT use this for order-related queries — use lookup_order instead.
    """
    ...

@mcp.tool()
def lookup_order(order_id: str) -> dict:
    """Look up an order by order number.

    Use this tool when the user asks about an order status, shipment
    tracking, delivery date, or order contents.

    Input: An order number in the format #XXXXX or ORD-XXXXX.
    Do NOT use this for customer profile queries — use get_customer instead.
    """
    ...

Notice what changed. Each description now says what the tool does, what input format it expects, when to use it, and when not to use it. That last part is surprisingly effective. Telling the model "don't use this for X, use Y instead" kills routing ambiguity.

MCP client integration

Connecting an MCP server to Claude Code is just a config entry:

json
{
  "mcpServers": {
    "demo-server": {
      "command": "python",
      "args": ["path/to/server.py"],
      "env": {}
    }
  }
}

For the API, the flow is a bit more involved. Your client:

  1. Connects to the MCP server
  2. Discovers available tools via the tools/list method
  3. Converts MCP tool schemas into the format the Messages API expects
  4. Passes them in the tools parameter when making requests

If that four-step handshake isn't clear yet, work through it with actual code. The exam tests this in Task 2.3 — MCP Client Integration.

Error handling in MCP tools

Tools break. Networks drop, APIs rate-limit, users send garbage inputs. Your MCP tools need to return error information the model can actually act on:

python
@mcp.tool()
def query_database(sql: str) -> str:
    """Execute a read-only SQL query against the analytics database.

    Args:
        sql: A SELECT query. INSERT, UPDATE, DELETE are rejected.
    """
    if not sql.strip().upper().startswith("SELECT"):
        return "Error: Only SELECT queries are allowed. Received a non-SELECT statement."

    try:
        result = db.execute(sql)
        return json.dumps(result.rows)
    except DatabaseError as e:
        return f"Error: Query failed — {e}. Check table and column names against the schema."

See how each error message tells the model what went wrong and what to try next? That's the difference between a tool Claude can recover from and one that sends it into a retry loop. "Error occurred" gives the model nothing. "Only SELECT queries are allowed" gives it a fix.

This pattern is covered in Task 2.4 — Tool Error Handling.

When to split tools

Should you build one big tool that handles multiple operations, or several focused ones? The exam has opinions about this.

Split when:

  • A catch-all tool like analyze_document is being called for fundamentally different jobs (extraction, summarisation, verification)
  • The description keeps using "or" to cover its use cases
  • Different operations need different input schemas

So analyze_document becomes extract_data_points, summarize_content, and verify_claim_against_source. Each one gets a tight, unambiguous description.

Don't split when:

  • Operations are closely related and take the same input
  • Splitting would produce tools with nearly identical descriptions (you've just moved the routing problem)
  • You'd end up with so many tools that the model struggles to choose between them

Exam patterns to watch for

Five things that come up repeatedly:

  1. "What's the most effective first step?" Nearly always: improve the tool descriptions. Routing classifiers, consolidation, few-shot examples are all valid options, but they're higher-effort and the exam wants you to reach for the simplest fix first.

  2. Description quality scenarios. You'll get a setup where two tools are being misrouted and need to identify vague descriptions as the root cause. Know the pattern from the examples above.

  3. Transport selection. stdio for local/subprocess use. HTTP+SSE for remote services. That's it.

  4. Tool vs Resource. Tools have side effects. Resources are read-only. The exam tests whether you know which is which.

  5. System prompt conflicts. Keyword-sensitive instructions in system prompts can override well-written tool descriptions. The exam wants you to recognise when a system prompt is fighting with your tool design.

Dig into the full Tool Design & MCP domain for all five task statements, or jump straight to the practice questions and see where you stand.