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.
This lesson imports from jsonrpc.py (Lesson 3). Make sure
all previous modules are in your project directory.
Your Project Structure
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
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:
"""
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:
# =============================================================================
# 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:
# =============================================================================
# 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
python resources.py
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:
"""
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
python file_example.py
File Resource Server
Resources: readme, package
Running on http://localhost:8080
Test 1: Initialize + List Resources
In another terminal, run the full MCP handshake:
# 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
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)
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.pyfor error handling - Works with
mcp_core.pyrouter - Can be combined with tools in the same server
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.