Lesson 6 - Step 5 of 5

Building File Resources

Build a complete MCP server that exposes local files as resources.

Building on Previous Lessons

In this step, we'll create resources.py — a reusable module for exposing resources. Then we'll create a file server example.

Building on Lessons 3-5

This lesson imports from jsonrpc.py (Lesson 3). Make sure all previous modules are in your project directory.

Your Project Structure

text
your_project/
    jsonrpc.py          # Lesson 3
    mcp_core.py         # Lesson 4
    tools.py            # Lesson 5
    resources.py        # NEW - We'll create this
    file_example.py     # NEW - Example using resources.py
How to Read Code Blocks

In this lesson:

  • Filename in header = Write this code to that file
  • "Add after Section X" = Append to your existing file
  • bash blocks = Commands to run in your terminal
  • "Expected Output" = What you should see (don't write this)

Section 1: Create resources.py

Create: resources.py

Create a new file with imports from Lesson 3:

python resources.py
"""
resources.py - Reusable resource registry for MCP servers.

This module builds on jsonrpc.py from Lesson 3.
"""

from typing import Callable

# Import error handling from Lesson 3
from jsonrpc import JsonRpcError, JsonRpcErrorCode


# =============================================================================
# Section 1: Resource Registry
# =============================================================================

class ResourceRegistry:
    """
    Registry for MCP resources with decorator-based registration.

    Usage:
        resources = ResourceRegistry()

        @resources.register(
            uri="file://config.json",
            name="Config",
            description="Configuration file",
            mime_type="application/json"
        )
        def read_config():
            return {"uri": "file://config.json", "mimeType": "application/json", "text": "{}"}
    """

    def __init__(self):
        self._resources: dict[str, dict] = {}
        self._readers: dict[str, Callable] = {}

    def register(
        self,
        uri: str,
        name: str,
        description: str,
        mime_type: str = "text/plain"
    ) -> Callable:
        """Decorator to register a resource reader."""
        def decorator(func: Callable[[], dict]) -> Callable:
            self._resources[uri] = {
                "uri": uri,
                "name": name,
                "description": description,
                "mimeType": mime_type
            }
            self._readers[uri] = func
            return func
        return decorator

    def list_resources(self) -> list[dict]:
        """Return all resource definitions for resources/list."""
        return list(self._resources.values())

    def read_resource(self, uri: str) -> dict:
        """Read a resource by URI."""
        if uri not in self._readers:
            raise JsonRpcError(
                JsonRpcErrorCode.INVALID_PARAMS,
                f"Resource not found: {uri}"
            )
        content = self._readers[uri]()
        return {"contents": [content]}

Section 2: Router Integration

Edit: resources.py

Add router integration after Section 1:

python resources.py - Add after Section 1
# =============================================================================
# Section 2: MCP Router Integration
# =============================================================================

def register_resource_handlers(router, resources: ResourceRegistry) -> None:
    """
    Register resources/list and resources/read handlers with an MCP router.

    Args:
        router: A JsonRpcRouter from mcp_core.py
        resources: A ResourceRegistry with registered resources
    """

    @router.method("resources/list")
    def handle_resources_list(params: dict) -> dict:
        return {"resources": resources.list_resources()}

    @router.method("resources/read")
    def handle_resources_read(params: dict) -> dict:
        uri = params.get("uri", "")
        return resources.read_resource(uri)

Add a test block at the end of resources.py:

python resources.py - Add at the end
# =============================================================================
# Test the module
# =============================================================================

if __name__ == "__main__":
    resources = ResourceRegistry()

    @resources.register(
        uri="config://settings",
        name="Settings",
        description="Application settings",
        mime_type="application/json"
    )
    def read_settings():
        return {"uri": "config://settings", "mimeType": "application/json", "text": '{"debug": true}'}

    print("Registered resources:")
    for r in resources.list_resources():
        print(f"  - {r['name']}: {r['uri']}")

    print("\nReading resource:")
    result = resources.read_resource("config://settings")
    print(f"  {result}")

Try It

bash
python resources.py
text Expected Output
Registered resources:
  - Settings: config://settings

Reading resource:
  {'contents': [{'uri': 'config://settings', 'mimeType': 'application/json', 'text': '{"debug": true}'}]}

Section 3: File Example

Create: file_example.py

Create a file server that uses resources.py and mcp_core.py:

python file_example.py
"""
file_example.py - File server using resources.py and mcp_core.py

Run: python file_example.py
"""

import json
from pathlib import Path
from http.server import HTTPServer, BaseHTTPRequestHandler

# Import from our modules
from jsonrpc import JsonRpcErrorCode, create_error_response
from mcp_core import router, handle_request
from resources import ResourceRegistry, register_resource_handlers

# =============================================================================
# Create resource registry and register file resources
# =============================================================================

file_resources = ResourceRegistry()


@file_resources.register(
    uri="file://readme",
    name="README",
    description="Project readme file",
    mime_type="text/markdown"
)
def read_readme():
    path = Path("README.md")
    if path.exists():
        return {"uri": "file://readme", "mimeType": "text/markdown", "text": path.read_text()}
    return {"uri": "file://readme", "mimeType": "text/plain", "text": "No README found"}


@file_resources.register(
    uri="file://package",
    name="package.json",
    description="Package configuration",
    mime_type="application/json"
)
def read_package():
    path = Path("package.json")
    if path.exists():
        return {"uri": "file://package", "mimeType": "application/json", "text": path.read_text()}
    return {"uri": "file://package", "mimeType": "application/json", "text": "{}"}


# Register resource handlers with the MCP router
register_resource_handlers(router, file_resources)


# =============================================================================
# HTTP Server
# =============================================================================

class MCPHandler(BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        print(f"[Files] {args[0]}")

    def send_json(self, data: dict, status: int = 200):
        body = json.dumps(data, indent=2).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(length)

        try:
            request = json.loads(body)
        except json.JSONDecodeError as e:
            self.send_json(create_error_response(None, JsonRpcErrorCode.PARSE_ERROR, str(e)), 400)
            return

        response = handle_request(request)
        if response is None:
            self.send_response(204)
            self.end_headers()
        else:
            self.send_json(response)


if __name__ == "__main__":
    print("File Resource Server")
    print("Resources: readme, package")
    print("Running on http://localhost:8080\n")

    server = HTTPServer(("localhost", 8080), MCPHandler)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nShutting down...")

Section 4: Test Your Server

Start the Server

bash
python file_example.py
text Expected Output
File Resource Server
Resources: readme, package
Running on http://localhost:8080

Test 1: Initialize + List Resources

In another terminal, run the full MCP handshake:

bash
# Initialize
curl -X POST http://localhost:8080 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","clientInfo":{"name":"test"}}}'

# Initialized notification
curl -X POST http://localhost:8080 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

# List resources
curl -X POST http://localhost:8080 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"resources/list"}'

Test 2: Read a Resource

bash
curl -X POST http://localhost:8080 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"resources/read","params":{"uri":"file://readme"}}'

Test 3: Read Invalid Resource (Error)

bash
curl -X POST http://localhost:8080 \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"file://nonexistent"}}'

Expected: JSON-RPC error with "Resource not found".

What You've Built

You've created a reusable resource system:

File Purpose
resources.py Reusable ResourceRegistry with decorator pattern
file_example.py Example server using all previous modules

The pattern mirrors tools.py from Lesson 5:

  • Decorator-based registration for clean resource definitions
  • Imports from jsonrpc.py for error handling
  • Works with mcp_core.py router
  • Can be combined with tools in the same server
Combining Tools and Resources

You can combine tools.py and resources.py in one server. Just import both and call both registration functions with the same router.

What's Next?

In Lesson 7, we'll explore the STDIO transport — an alternative to HTTP that's perfect for local integrations. You'll learn how to build MCP servers that communicate through standard input/output, enabling seamless integration with desktop applications like Claude Desktop.