Lesson 6 - Step 4 of 5

The resources/read Method

Implement reading resource contents with text and blob support.

Purpose of resources/read

The resources/read method retrieves the actual content of a resource. After listing resources, clients use this method to fetch the data they need.

Request Format

The client specifies which resource to read by its URI:

json
{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "resources/read",
    "params": {
        "uri": "file:///project/config.json"
    }
}

Response Format

The server returns a contents array. Each item contains the resource URI, optional MIME type, and the content as either text or blob.

json
{
    "jsonrpc": "2.0",
    "id": 2,
    "result": {
        "contents": [
            {
                "uri": "file:///project/config.json",
                "mimeType": "application/json",
                "text": "{\n  \"debug\": true,\n  \"port\": 8080\n}"
            }
        ]
    }
}

Text vs Blob Content

MCP supports two content types: text for human-readable content and blob for binary data.

Field Type Use For
text UTF-8 string Source code, JSON, markdown, plain text
blob Base64-encoded string Images, PDFs, binary files
Choose One: text or blob

Each content item must have either text or blob, never both. Use text when possible — it's more efficient and the AI can read it directly.

Text Content Example

json
{
    "jsonrpc": "2.0",
    "id": 2,
    "result": {
        "contents": [
            {
                "uri": "file:///project/main.py",
                "mimeType": "text/x-python",
                "text": "def main():\n    print('Hello, World!')\n\nif __name__ == '__main__':\n    main()"
            }
        ]
    }
}

Blob Content Example

For binary content, encode it as Base64:

json
{
    "jsonrpc": "2.0",
    "id": 3,
    "result": {
        "contents": [
            {
                "uri": "file:///project/logo.png",
                "mimeType": "image/png",
                "blob": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
            }
        ]
    }
}

Implementing resources/read in Python

python
"""
resources_read.py - Implementing resources/read
"""

import base64
from pathlib import Path

# Resource registry with content providers
RESOURCES = {
    "file:///project/config.json": {
        "name": "config.json",
        "mimeType": "application/json"
    },
    "file:///project/README.md": {
        "name": "README.md",
        "mimeType": "text/markdown"
    },
    "file:///project/logo.png": {
        "name": "logo.png",
        "mimeType": "image/png"
    }
}


def is_binary_mime_type(mime_type: str) -> bool:
    """Check if a MIME type represents binary content."""
    binary_types = [
        "image/", "audio/", "video/",
        "application/octet-stream",
        "application/pdf",
        "application/zip"
    ]
    return any(mime_type.startswith(t) for t in binary_types)


def read_file_content(uri: str) -> dict:
    """Read file content and return appropriate format."""
    # Extract file path from URI
    if uri.startswith("file://"):
        file_path = Path(uri[7:])  # Remove "file://" prefix
    else:
        raise ValueError(f"Unsupported URI scheme: {uri}")

    resource = RESOURCES.get(uri)
    if not resource:
        raise ValueError(f"Unknown resource: {uri}")

    mime_type = resource.get("mimeType", "application/octet-stream")

    # Read the file
    if is_binary_mime_type(mime_type):
        # Binary content - encode as base64
        with open(file_path, "rb") as f:
            content = base64.b64encode(f.read()).decode("utf-8")
        return {
            "uri": uri,
            "mimeType": mime_type,
            "blob": content
        }
    else:
        # Text content - read as UTF-8
        with open(file_path, "r", encoding="utf-8") as f:
            content = f.read()
        return {
            "uri": uri,
            "mimeType": mime_type,
            "text": content
        }


def handle_resources_read(params: dict) -> dict:
    """Handle resources/read requests."""
    uri = params.get("uri", "")

    if not uri:
        raise ValueError("Missing required parameter: uri")

    content = read_file_content(uri)

    return {
        "contents": [content]
    }

Error Handling

When a resource cannot be read, return a JSON-RPC error with an appropriate code:

python
"""
error_handling.py - Proper error responses for resources/read
"""


def handle_resources_read(params: dict) -> dict:
    """Handle resources/read with proper error handling."""
    uri = params.get("uri", "")

    if not uri:
        raise MCPError(-32602, "Missing required parameter: uri")

    if uri not in RESOURCES:
        raise MCPError(-32602, f"Resource not found: {uri}")

    try:
        content = read_file_content(uri)
        return {"contents": [content]}
    except FileNotFoundError:
        raise MCPError(-32002, f"File not found: {uri}")
    except PermissionError:
        raise MCPError(-32003, f"Permission denied: {uri}")
    except UnicodeDecodeError:
        raise MCPError(-32004, f"Cannot decode file as text: {uri}")


class MCPError(Exception):
    """Custom exception for MCP errors."""
    def __init__(self, code: int, message: str):
        self.code = code
        self.message = message
        super().__init__(message)
json
// Error response when resource not found
{
    "jsonrpc": "2.0",
    "id": 2,
    "error": {
        "code": -32602,
        "message": "Resource not found: file:///nonexistent.txt"
    }
}

Complete Handler Integration

Here's how to integrate both methods into your server:

python
"""
complete_handler.py - Full resources implementation
"""


def route_request(method: str, params: dict) -> dict:
    """Route requests to appropriate handlers."""
    handlers = {
        "initialize": handle_initialize,
        "resources/list": handle_resources_list,
        "resources/read": handle_resources_read,
    }

    handler = handlers.get(method)
    if not handler:
        raise MCPError(-32601, f"Method not found: {method}")

    return handler(params)


def handle_initialize(params: dict) -> dict:
    """Handle initialization with resource capability."""
    return {
        "protocolVersion": "2024-11-05",
        "capabilities": {
            "resources": {}  # Advertise resource support
        },
        "serverInfo": {
            "name": "resource-server",
            "version": "1.0.0"
        }
    }

Testing resources/read

bash
# Read a text resource
curl -X POST http://localhost:8080 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "resources/read",
    "params": {
        "uri": "file:///project/config.json"
    }
  }'
Handling Large Resources

For very large files, consider implementing streaming or chunked responses. The standard resources/read method is best suited for resources that fit comfortably in memory.