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

  1. “What is the difference between MCP tools and resources?”
    • Tools perform actions with potential side effects; resources provide read-only data.
  2. “How would you design a URI scheme for structured data access?”
    • Discuss scheme naming, path hierarchy, templates, and query parameters.
  3. “How do you handle large resources that do not fit in context?”
    • Explain pagination, truncation, summarization strategies.
  4. “What is a resource template and when would you use one?”
    • Templates enable parameterized resource access like config://env/{env}.
  5. “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:

  1. Add resource caching with TTL for expensive operations
  2. Implement search across resources with query parameters
  3. Add resource subscriptions for change notifications
  4. Support binary resources like images with base64 encoding
  5. Implement resource versioning for historical access

Common Pitfalls

  1. Forgetting list_resources - Claude cannot discover resources without it
  2. Inconsistent URI schemes - Pick a convention and stick to it
  3. Not handling missing resources - Always return helpful error messages
  4. Returning too much data - Resources should fit in context
  5. 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