Intermediate

Understanding MCP: Architecture and Concepts

Lesson 1 of 4 Estimated Time 50 min

Understanding MCP: Architecture and Concepts

The Model Context Protocol (MCP) is a standardized architecture for connecting language models to external data sources and tools. Unlike ad-hoc integrations, MCP provides a systematic way to extend AI capabilities while maintaining security and standardization. This lesson explores MCP’s architecture, core concepts, and how it enables seamless integration.

Why Model Context Protocol

Traditional approaches to extending AI capabilities are fragmented:

  • Tight Coupling: Custom integration code for each data source
  • Maintenance Burden: Changes in external systems require code updates
  • Security Challenges: Managing credentials and access control scattered across codebase
  • Limited Reusability: Integration code specific to one application

MCP solves these problems by providing:

  • Standard Interface: Consistent protocol for all integrations
  • Decoupled Architecture: Client and server operate independently
  • Composable Resources: Combine multiple data sources seamlessly
  • Security First: Built-in authentication and authorization mechanisms

MCP Architecture Overview

MCP follows a client-server architecture where clients (like Claude) communicate with servers (exposing resources and tools):

┌─────────────────────────────────┐
│        Client (Claude)          │
│  - Sends requests to servers    │
│  - Processes responses          │
│  - Orchestrates multi-server    │
│    interactions                 │
└──────────────┬──────────────────┘

        ┌──────┴──────┐
        │   MCP       │
        │ Protocol    │
        └──────┬──────┘

      ┌────────┼────────┐
      │        │        │
┌─────▼──┐ ┌──▼────┐ ┌─▼──────┐
│Server 1│ │Server2│ │Server 3│
│ (DB)   │ │(API)  │ │(Files) │
└────────┘ └───────┘ └────────┘

Core Components

Transport Layer: Handles communication between client and server. Supports:

  • Standard input/output (stdio)
  • Server-sent events (SSE) over HTTP
  • WebSockets for real-time communication

Request-Response Model: Clients send requests, servers respond:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "resources/list",
  "params": {}
}

Resources: Named, addressable pieces of data or capabilities:

  • Documents
  • Database records
  • API endpoints
  • File paths

Tools: Functions that servers expose for client invocation.

Prompts: Pre-written instructions or templates that servers provide.

MCP Communication Protocol

JSON-RPC 2.0 Foundation

MCP uses JSON-RPC 2.0 for request-response communication:

import json
from typing import Dict, Any

class MCPMessage:
    """Represents an MCP protocol message."""

    def __init__(self, method: str, params: Dict[str, Any] = None, msg_id: int = 1):
        self.method = method
        self.params = params or {}
        self.id = msg_id

    def to_json(self) -> str:
        """Convert to JSON-RPC format."""
        return json.dumps({
            "jsonrpc": "2.0",
            "id": self.id,
            "method": self.method,
            "params": self.params
        })

# Example: Request to list available resources
list_resources_msg = MCPMessage("resources/list")
print(list_resources_msg.to_json())
# Output: {"jsonrpc": "2.0", "id": 1, "method": "resources/list", "params": {}}

Initialization Handshake

Clients and servers exchange capabilities during initialization:

class ServerCapabilities:
    """Define what a server can provide."""

    def __init__(self):
        self.capabilities = {
            "resources": {
                "subscribe": False  # Can client subscribe to resource updates?
            },
            "tools": {},
            "prompts": {}
        }

    def to_dict(self) -> Dict:
        return {
            "serverInfo": {
                "name": "example-server",
                "version": "1.0.0"
            },
            "capabilities": self.capabilities
        }

# Server initialization response
server_init = ServerCapabilities()
print(json.dumps(server_init.to_dict(), indent=2))

Resources: Data Access Layer

Resources represent accessible data or information that servers provide:

Resource Definition

@dataclass
class Resource:
    """Represents a resource provided by server."""
    uri: str  # Unique identifier, e.g., "sqlite:///database.db/users"
    name: str
    description: str
    resource_type: str = "text/plain"

class ResourceServer:
    """Server that provides resources."""

    def __init__(self):
        self.resources = [
            Resource(
                uri="sqlite:///company.db/employees",
                name="Employee Database",
                description="Access employee records and information"
            ),
            Resource(
                uri="file:///documents/policies.md",
                name="Company Policies",
                description="Employee handbook and policies"
            ),
            Resource(
                uri="https://api.company.com/announcements",
                name="Company Announcements",
                description="Latest company announcements and news"
            )
        ]

    def list_resources(self) -> list:
        """Return available resources."""
        return [{
            "uri": r.uri,
            "name": r.name,
            "description": r.description,
            "mimeType": r.resource_type
        } for r in self.resources]

    def read_resource(self, uri: str) -> str:
        """Read content of a resource."""
        for resource in self.resources:
            if resource.uri == uri:
                # In real implementation, fetch actual data
                return f"Content of {resource.name}"
        raise ValueError(f"Resource not found: {uri}")

Resource URIs and Hierarchy

Resources use URI format for global addressing:

def parse_resource_uri(uri: str) -> Dict[str, str]:
    """Parse MCP resource URI."""
    # Format: scheme://authority/path
    # Examples:
    # - sqlite:///database.db/users
    # - file:///home/user/documents/report.pdf
    # - http://api.example.com/v1/data

    import urllib.parse

    parsed = urllib.parse.urlparse(uri)

    return {
        "scheme": parsed.scheme,  # e.g., "sqlite", "file", "http"
        "authority": parsed.netloc,  # e.g., "", "api.example.com"
        "path": parsed.path,  # e.g., "/database.db/users"
        "query": parsed.query
    }

# Examples
print(parse_resource_uri("sqlite:///database.db/users"))
# {'scheme': 'sqlite', 'authority': '', 'path': '/database.db/users', 'query': ''}

print(parse_resource_uri("http://api.example.com/v1/data?limit=10"))
# {'scheme': 'http', 'authority': 'api.example.com', 'path': '/v1/data', 'query': 'limit=10'}

Tools: Function Invocation

Tools allow clients to invoke server functions:

Tool Definition

class Tool:
    """Defines a tool available on server."""

    def __init__(self, name: str, description: str, input_schema: Dict):
        self.name = name
        self.description = description
        self.input_schema = input_schema

    def to_dict(self) -> Dict:
        return {
            "name": self.name,
            "description": self.description,
            "inputSchema": self.input_schema
        }

class ToolServer:
    """Server exposing tools."""

    def __init__(self):
        self.tools = [
            Tool(
                name="search_documents",
                description="Search through company documents",
                input_schema={
                    "type": "object",
                    "properties": {
                        "query": {
                            "type": "string",
                            "description": "Search query"
                        },
                        "limit": {
                            "type": "integer",
                            "description": "Maximum results"
                        }
                    },
                    "required": ["query"]
                }
            ),
            Tool(
                name="create_issue",
                description="Create a ticket in issue tracker",
                input_schema={
                    "type": "object",
                    "properties": {
                        "title": {"type": "string"},
                        "description": {"type": "string"},
                        "priority": {
                            "type": "string",
                            "enum": ["low", "medium", "high"]
                        }
                    },
                    "required": ["title", "description"]
                }
            )
        ]

    def list_tools(self) -> list:
        """Return available tools."""
        return [t.to_dict() for t in self.tools]

    def call_tool(self, name: str, arguments: Dict) -> str:
        """Execute a tool."""
        if name == "search_documents":
            query = arguments.get("query")
            limit = arguments.get("limit", 10)
            return f"Found documents matching '{query}' (limited to {limit})"
        elif name == "create_issue":
            title = arguments.get("title")
            return f"Created issue: {title}"
        else:
            raise ValueError(f"Unknown tool: {name}")

Tool Invocation Flow

class MCPClient:
    """Client that uses MCP to invoke tools."""

    def __init__(self, server: ToolServer):
        self.server = server

    def discover_tools(self) -> list:
        """Discover what tools server provides."""
        return self.server.list_tools()

    def invoke_tool(self, name: str, arguments: Dict) -> str:
        """Invoke a tool on the server."""
        # Validate tool exists
        tools = {t["name"]: t for t in self.discover_tools()}

        if name not in tools:
            raise ValueError(f"Tool not found: {name}")

        tool = tools[name]

        # Validate arguments match schema
        # (In production, use jsonschema library)

        # Invoke tool
        return self.server.call_tool(name, arguments)

# Usage
server = ToolServer()
client = MCPClient(server)

# Discover tools
print("Available tools:")
for tool in client.discover_tools():
    print(f"  - {tool['name']}: {tool['description']}")

# Invoke a tool
result = client.invoke_tool("search_documents", {"query": "budget", "limit": 5})
print(result)

Prompts: Pre-Written Instructions

Prompts allow servers to provide templates or instructions:

class Prompt:
    """Server-provided prompt template."""

    def __init__(self, name: str, description: str, template: str):
        self.name = name
        self.description = description
        self.template = template
        self.arguments = []

    def to_dict(self) -> Dict:
        return {
            "name": self.name,
            "description": self.description
        }

class PromptServer:
    """Server providing prompt templates."""

    def __init__(self):
        self.prompts = [
            Prompt(
                name="code_review",
                description="Template for code review guidelines",
                template="""Review this code for:
1. Correctness and logic
2. Performance considerations
3. Security vulnerabilities
4. Code style and readability

Code to review:
{code}"""
            ),
            Prompt(
                name="bug_report",
                description="Template for comprehensive bug reports",
                template="""Please provide:
- Steps to reproduce
- Expected behavior
- Actual behavior
- System information

Report: {description}"""
            )
        ]

    def list_prompts(self) -> list:
        """List available prompts."""
        return [p.to_dict() for p in self.prompts]

    def get_prompt(self, name: str, arguments: Dict) -> str:
        """Get rendered prompt."""
        for prompt in self.prompts:
            if prompt.name == name:
                return prompt.template.format(**arguments)
        raise ValueError(f"Prompt not found: {name}")

Transport Mechanisms

Stdio Transport

Simple transport using standard input/output:

import sys
import json

class StdioMCPServer:
    """MCP server using stdio."""

    def __init__(self):
        self.running = True

    def run(self):
        """Read requests from stdin, send responses to stdout."""
        while self.running:
            try:
                line = sys.stdin.readline()
                if not line:
                    break

                request = json.loads(line)
                response = self.handle_request(request)
                print(json.dumps(response))
                sys.stdout.flush()

            except Exception as e:
                error_response = {
                    "jsonrpc": "2.0",
                    "id": request.get("id"),
                    "error": {
                        "code": -32603,
                        "message": str(e)
                    }
                }
                print(json.dumps(error_response))
                sys.stdout.flush()

    def handle_request(self, request: Dict) -> Dict:
        """Handle an MCP request."""
        method = request.get("method")
        params = request.get("params", {})
        msg_id = request.get("id")

        if method == "resources/list":
            return self._list_resources(msg_id)
        elif method == "tools/list":
            return self._list_tools(msg_id)
        else:
            return {
                "jsonrpc": "2.0",
                "id": msg_id,
                "error": {"code": -32601, "message": "Method not found"}
            }

    def _list_resources(self, msg_id: int) -> Dict:
        """Handle resources/list request."""
        return {
            "jsonrpc": "2.0",
            "id": msg_id,
            "result": {
                "resources": [
                    {
                        "uri": "memory:///notes",
                        "name": "Notes",
                        "description": "User notes"
                    }
                ]
            }
        }

    def _list_tools(self, msg_id: int) -> Dict:
        """Handle tools/list request."""
        return {
            "jsonrpc": "2.0",
            "id": msg_id,
            "result": {
                "tools": []
            }
        }

Best Practices

1. Resource Organization

Group related resources with meaningful URI hierarchies:

database:///company/employees
database:///company/departments
database:///company/projects

2. Clear Tool Specifications

Define tools with precise input schemas and descriptions.

3. Error Handling

Return proper JSON-RPC error responses with meaningful messages.

Key Takeaway

MCP provides a standardized, composable architecture for connecting language models to external systems. Understanding resources, tools, and transports enables building scalable, maintainable AI integrations without tight coupling or scattered integration logic.

Exercises

  1. Build a Resource Server: Create an MCP server that exposes multiple resources (files, database records, API endpoints).

  2. Tool Implementation: Implement a tool server with 3-5 tools that perform different operations.

  3. Stdio Transport: Write a complete stdio-based MCP server that handles multiple request types.

  4. Resource URIs: Design a comprehensive URI hierarchy for a fictional company’s resources.

  5. Integration: Integrate your server with an MCP-compatible client and test resource access and tool invocation.