Project 17: Custom MCP Resource Provider
Project 17: Custom MCP Resource Provider
Project Overview
| Attribute | Value |
|---|---|
| File | P17-mcp-resource-provider.md |
| Main Programming Language | Python |
| Alternative Programming Languages | TypeScript, Go |
| Coolness Level | Level 3: Genuinely Clever |
| Business Potential | 2. The “Micro-SaaS / Pro Tool” |
| Difficulty | Level 3: Advanced |
| Knowledge Area | MCP / Resources / Data Providers |
| Software or Tool | MCP SDK, Various data sources |
| Main Book | “Designing Data-Intensive Applications” by Martin Kleppmann |
| Time Estimate | 1-2 weeks |
| Prerequisites | Project 15-16 completed, URI/URL understanding |
What You Will Build
An MCP server that exposes resources (not just tools)–structured data that Claude can read and reference. Examples include: configuration files, environment variables, API documentation, and project metadata. Uses the @ syntax for resource access.
MCP has two main concepts: tools (actions) and resources (data). This project focuses on resources, teaching you how to expose read-only data that Claude can incorporate into its context.
Real World Outcome
You: @config://env/production
Claude: [Loads resource config://env/production]
I have loaded your production environment configuration:
DATABASE_URL: postgres://prod-db.example.com/app
REDIS_URL: redis://prod-cache.example.com
LOG_LEVEL: warn
FEATURE_FLAGS:
new_checkout: true
beta_api: false
You: Compare that with @config://env/staging
Claude: [Loads resource config://env/staging]
Comparing production vs staging:
| Setting | Production | Staging |
|--------------------------|-------------|-------------|
| DATABASE_URL | prod-db | staging-db |
| LOG_LEVEL | warn | debug |
| FEATURE_FLAGS.beta_api | false | true |
Key differences:
- Staging has debug logging enabled
- Staging has beta_api flag enabled (production disabled)
The Core Question You Are Answering
“How do I expose structured data as MCP resources that Claude can reference with the @ syntax?”
Tools are for actions; resources are for data. By creating resource providers, you give Claude access to structured information it can reason about–configuration files, documentation, metadata, and more.
MCP Tools vs Resources
+------------------------------------------------------------------+
| MCP CONCEPT COMPARISON |
+------------------------------------------------------------------+
TOOLS (Actions) RESOURCES (Data)
+------------------+ +------------------+
| Has side effects | | Read-only |
| Invoked by name | | Referenced by URI|
| Returns results | | Returns content |
| Claude calls | | Claude reads |
+------------------+ +------------------+
Example: Example:
mcp__github__create_pr @config://env/production
mcp__sqlite__query @docs://api/users/create
When to use TOOLS: When to use RESOURCES:
- Database queries - Configuration files
- API calls - Documentation
- File modifications - Environment variables
- External service - Project metadata
interactions - Static reference data
Resource URI Architecture
+------------------------------------------------------------------+
| RESOURCE URI STRUCTURE |
+------------------------------------------------------------------+
config://env/production?format=yaml
| | | |
+-------+ +----------+----------+
| | |
Scheme Path Query Params
(custom) (hierarchical) (optional)
Examples:
+-------------------------------------------------------------+
| URI | Description |
|------------------------------|-------------------------------|
| config://env/production | Production env config |
| config://env/staging | Staging env config |
| docs://api/users | Users API documentation |
| docs://api/users/create | Specific endpoint docs |
| project://info | Project metadata |
| project://dependencies | Package dependencies |
| git://log/10 | Last 10 commits |
| git://diff/main..HEAD | Diff between branches |
+-------------------------------------------------------------+
URI Template (RFC 6570):
config://env/{environment}
^^^^^^^^^^^
Template parameter
Resource Provider Flow
+------------------------------------------------------------------+
| RESOURCE ACCESS FLOW |
+------------------------------------------------------------------+
User types: @config://env/production
|
v
+---------------------------------------+
| CLAUDE CODE |
| |
| 1. Parse @ reference |
| 2. Match to MCP server |
| 3. Send read_resource request |
+---------------------------------------+
|
| JSON-RPC: resources/read
| params: { uri: "config://env/production" }
v
+---------------------------------------+
| YOUR MCP SERVER |
| |
| 1. Parse URI scheme and path |
| 2. Load data from source |
| 3. Format as content |
| 4. Return to Claude |
+---------------------------------------+
|
v
+---------------------------------------+
| Claude incorporates resource |
| content into its context |
+---------------------------------------+
Concepts You Must Understand First
Stop and research these before coding:
1. MCP Resources vs Tools
| Aspect | Tools | Resources |
|---|---|---|
| Purpose | Perform actions | Provide data |
| Side effects | Yes (may modify state) | No (read-only) |
| Invocation | Claude calls by name | User references by URI |
| Return type | Action result | Content blob |
| Example | query(sql) |
@config://db/schema |
2. URI Schemes (RFC 3986)
userinfo host port
┌──┴───┐ ┌──────┴──────┐ ┌┴┐
https://john.doe@www.example.com:123/forum/questions/?tag=networking#top
└─┬─┘ └───────────┬──────────────┘└───────┬───────┘ └───────┬─────┘ └─┬─┘
scheme authority path query fragment
For MCP Resources (custom schemes):
config://env/production?format=yaml
└──┬──┘ └──────┬──────┘└─────┬────┘
scheme path query
Custom scheme conventions:
- Lowercase, descriptive: config, docs, project, git
- Path is hierarchical: /category/subcategory/item
- Query params for options: ?format=yaml&include_comments=true
3. Resource Templates
Templates allow dynamic resource generation:
# Static resource (no template)
"project://info" # Always returns same data
# Template resource (parameterized)
"config://env/{environment}"
# ^^^^^^^^^^^
# Parameter replaced at runtime
#
# Valid URIs:
# config://env/production
# config://env/staging
# config://env/development
Questions to Guide Your Design
Before implementing, think through these:
1. What Resources to Expose?
| Resource Type | URI Scheme | Data Source |
|---|---|---|
| Environment configs | config://env/{name} |
.env files |
| API documentation | docs://api/{endpoint} |
OpenAPI/markdown |
| Project metadata | project://info |
package.json |
| Dependencies | project://deps |
requirements.txt |
| Git history | git://log/{count} |
git log output |
| Database schema | schema://tables/{name} |
DB introspection |
2. URI Design Decisions
- What scheme prefix? Use descriptive names:
config://,docs://,project:// - How to represent hierarchy? Path segments:
/category/subcategory/item - How to handle parameters? Templates:
{param}or query strings:?key=value - Case sensitivity? Typically lowercase for consistency
3. Large Resource Handling
| Strategy | When to Use | Example |
|---|---|---|
| Pagination | Large lists | git://log/100?page=2 |
| Truncation | Long content | First 10KB with “…truncated” |
| Summarization | Complex data | Aggregate stats instead of raw |
| Streaming | Very large | Return chunks progressively |
Thinking Exercise
Design Your Resource Schema Before Implementing
# Resource definitions for your server
resources = {
# Environment configuration (templated)
"config://env/{environment}": {
"description": "Environment-specific configuration",
"mimeType": "application/yaml",
"template": True,
"params": {
"environment": {
"type": "string",
"enum": ["production", "staging", "development"]
}
}
},
# API documentation (templated)
"docs://api/{endpoint}": {
"description": "API endpoint documentation",
"mimeType": "text/markdown",
"template": True,
"params": {
"endpoint": {
"type": "string",
"description": "API endpoint path like 'users/create'"
}
}
},
# Project info (static)
"project://info": {
"description": "Project metadata from package.json/pyproject.toml",
"mimeType": "application/json",
"template": False
},
# Git log (templated)
"git://log/{count}": {
"description": "Recent git commits",
"mimeType": "text/plain",
"template": True,
"params": {
"count": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 10
}
}
}
}
Questions to consider:
- How does Claude discover available resources? (Implement
list_resources) - What happens if a template parameter is invalid?
- Should you cache resource content for performance?
- How do you handle missing/not-found resources?
The Interview Questions They Will Ask
- “What is the difference between MCP tools and resources?”
- Tools perform actions with potential side effects; resources provide read-only data.
- “How would you design a URI scheme for structured data access?”
- Discuss scheme naming, path hierarchy, templates, and query parameters.
- “How do you handle large resources that do not fit in context?”
- Explain pagination, truncation, summarization strategies.
- “What is a resource template and when would you use one?”
- Templates enable parameterized resource access like
config://env/{env}.
- Templates enable parameterized resource access like
- “How would you implement resource caching in an MCP server?”
- Discuss in-memory cache, TTL, cache invalidation strategies.
Hints in Layers
Hint 1: Implement list_resources First
Claude needs to discover what resources exist. The resources/list handler is essential:
@server.list_resources()
async def list_resources():
return [
Resource(
uri="config://env/production",
name="Production Environment",
mimeType="application/yaml"
),
Resource(
uri="config://env/staging",
name="Staging Environment",
mimeType="application/yaml"
),
# ... more resources
]
Hint 2: Use URI Templates
For dynamic resources, implement template parameter parsing:
import re
def parse_template(uri: str, template: str) -> dict:
"""Extract template parameters from URI."""
# Convert template to regex
pattern = template.replace("{", "(?P<").replace("}", ">[^/]+)")
match = re.match(f"^{pattern}$", uri)
return match.groupdict() if match else None
# Example:
parse_template("config://env/production", "config://env/{environment}")
# Returns: {"environment": "production"}
Hint 3: Handle Not Found Gracefully
Return a clear error when a resource does not exist:
@server.read_resource()
async def read_resource(uri: str):
if uri.startswith("config://env/"):
env = uri.split("/")[-1]
config_path = Path(f".env.{env}")
if not config_path.exists():
raise ValueError(f"Environment '{env}' not found. Available: production, staging, development")
return [TextContent(type="text", text=config_path.read_text())]
raise ValueError(f"Unknown resource scheme: {uri.split(':')[0]}")
Hint 4: Add Resource Hints for Autocomplete
Configure resource hints in .mcp.json so Claude suggests resources:
{
"mcpServers": {
"config": {
"type": "stdio",
"command": "python",
"args": ["config_server.py"],
"resourceHints": [
"config://env/production",
"config://env/staging",
"docs://api/*"
]
}
}
}
Books That Will Help
| Topic | Book | Chapter | Why It Helps |
|---|---|---|---|
| URI Design | “RESTful Web APIs” by Richardson | Ch. 4 | URL structure patterns |
| Data Serialization | “Designing Data-Intensive Applications” | Ch. 4 | Encoding and formats |
| Configuration Management | “The Twelve-Factor App” | Config section | Environment configuration |
| API Design | “Building Microservices” by Newman | Ch. 4 | Service interfaces |
Implementation Skeleton
#!/usr/bin/env python3
"""MCP Resource Provider - Expose structured data to Claude."""
import asyncio
import os
import json
import yaml
from pathlib import Path
from typing import Optional
from mcp.server import Server
from mcp.types import Resource, TextContent
from mcp.server.stdio import stdio_server
server = Server("resource-provider")
# Define available environments
ENVIRONMENTS = ["production", "staging", "development"]
@server.list_resources()
async def list_resources():
"""List all available resources."""
resources = []
# Environment configuration resources
for env in ENVIRONMENTS:
config_path = Path(f".env.{env}")
if config_path.exists():
resources.append(
Resource(
uri=f"config://env/{env}",
name=f"{env.capitalize()} Environment Configuration",
description=f"Environment variables for {env}",
mimeType="application/yaml"
)
)
# Project info resource
for pkg_file in ["package.json", "pyproject.toml", "Cargo.toml"]:
if Path(pkg_file).exists():
resources.append(
Resource(
uri="project://info",
name="Project Information",
description=f"Metadata from {pkg_file}",
mimeType="application/json"
)
)
break
# API documentation resources
docs_dir = Path("docs/api")
if docs_dir.exists():
for doc_file in docs_dir.glob("**/*.md"):
endpoint = str(doc_file.relative_to(docs_dir)).replace(".md", "")
resources.append(
Resource(
uri=f"docs://api/{endpoint}",
name=f"API: {endpoint}",
description=f"Documentation for {endpoint} endpoint",
mimeType="text/markdown"
)
)
return resources
@server.read_resource()
async def read_resource(uri: str):
"""Read and return resource content."""
# Handle config://env/{environment}
if uri.startswith("config://env/"):
env = uri.split("/")[-1]
if env not in ENVIRONMENTS:
raise ValueError(
f"Unknown environment: {env}. "
f"Available: {', '.join(ENVIRONMENTS)}"
)
config_path = Path(f".env.{env}")
if not config_path.exists():
raise ValueError(f"Configuration file for {env} not found: {config_path}")
# Parse .env file into YAML for readability
env_vars = {}
with open(config_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, value = line.split("=", 1)
env_vars[key.strip()] = value.strip()
content = yaml.dump(env_vars, default_flow_style=False)
return [TextContent(type="text", text=content)]
# Handle project://info
if uri == "project://info":
for pkg_file, loader in [
("package.json", json.load),
("pyproject.toml", lambda f: __import__("tomllib").load(f)),
]:
pkg_path = Path(pkg_file)
if pkg_path.exists():
with open(pkg_path, "rb" if "toml" in pkg_file else "r") as f:
data = loader(f)
return [TextContent(type="text", text=json.dumps(data, indent=2))]
raise ValueError("No package.json or pyproject.toml found")
# Handle docs://api/{endpoint}
if uri.startswith("docs://api/"):
endpoint = uri.replace("docs://api/", "")
doc_path = Path(f"docs/api/{endpoint}.md")
if not doc_path.exists():
raise ValueError(f"Documentation not found for endpoint: {endpoint}")
return [TextContent(type="text", text=doc_path.read_text())]
raise ValueError(f"Unknown resource URI: {uri}")
@server.list_resource_templates()
async def list_resource_templates():
"""List resource templates for discovery."""
return [
{
"uriTemplate": "config://env/{environment}",
"name": "Environment Configuration",
"description": "Configuration for a specific environment",
"mimeType": "application/yaml"
},
{
"uriTemplate": "docs://api/{endpoint}",
"name": "API Documentation",
"description": "Documentation for an API endpoint",
"mimeType": "text/markdown"
}
]
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
Learning Milestones
| Milestone | What It Proves | Verification |
|---|---|---|
| Resources are discoverable | You understand list_resources |
Ask Claude “What resources are available?” |
| @ syntax loads resources | You understand read_resource |
Type @config://env/production |
| Templates work | You can create dynamic resources | @config://env/staging works too |
| Errors are helpful | Production-ready | Invalid URIs give clear messages |
Core Challenges Mapped to Concepts
| Challenge | Concept | Book Reference |
|---|---|---|
| Defining resource URIs | URI scheme design | RFC 3986 |
| Resource templates | Dynamic resource generation | MCP Specification |
| Large resource handling | Pagination and streaming | “Designing Data-Intensive Applications” |
| Resource discovery | Listing and search | MCP SDK Documentation |
Extension Ideas
Once the basic server works, consider these enhancements:
- Add resource caching with TTL for expensive operations
- Implement search across resources with query parameters
- Add resource subscriptions for change notifications
- Support binary resources like images with base64 encoding
- Implement resource versioning for historical access
Common Pitfalls
- Forgetting list_resources - Claude cannot discover resources without it
- Inconsistent URI schemes - Pick a convention and stick to it
- Not handling missing resources - Always return helpful error messages
- Returning too much data - Resources should fit in context
- Not considering MIME types - Set appropriate types for formatting
Success Criteria
You have completed this project when:
- Your MCP server exposes multiple resource types
- Claude can list available resources
- The @ syntax loads resource content into context
- Resource templates work with different parameters
- Invalid URIs return helpful error messages
- Resources are formatted appropriately (YAML, JSON, Markdown)
- Large resources are handled gracefully
- Documentation exists for your URI scheme