Claude-skill-registry-data mcp-resource-exposure-patterns

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry-data
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry-data "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/mcp-resource-exposure-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-data-mcp-resource-exposure-patterns && rm -rf "$T"
manifest: data/mcp-resource-exposure-patterns/SKILL.md
source content

MCP Resource Exposure Patterns Skill

Metadata (Tier 1)

Keywords: resources, uri, context provisioning, list_resources, read_resource

File Patterns: **/resources/*.py, **/providers.py

Modes: backend_python


Instructions (Tier 2)

Resources vs Tools

Resources (app-controlled):

  • Provide read-only context to Claude
  • URI-based identification
  • Examples: documentation, logs, database schemas, API specs

Tools (model-controlled):

  • Perform actions with side effects
  • Require explicit invocation by Claude
  • Examples: create_file, execute_query, send_email

Rule: If it's read-only context provisioning → Resource. If it performs an action → Tool.

URI Patterns

Resources use URI schemes for identification:

# File resources
"file:///project/README.md"
"file:///docs/api/authentication.md"

# Database resources
"db:///schemas/users"
"db:///tables/orders/schema"

# API resources
"api:///endpoints/v1/users"
"api:///swagger.json"

# Log resources
"log:///app/2024-01-15"
"log:///errors/recent"

# Custom schemes
"config:///settings/database"
"metric:///cpu/usage"

list_resources Implementation

from pydantic import BaseModel, ConfigDict
from typing import Literal

class ResourceDescriptor(BaseModel):
    """Resource metadata returned by list_resources."""
    model_config = ConfigDict(strict=True)

    uri: str
    name: str
    mimeType: str
    description: str | None = None

@server.list_resources()
async def list_resources() -> list[dict]:
    """List all available resources."""
    resources = []

    # File-based resources
    docs_dir = Path("docs")
    if docs_dir.exists():
        for md_file in docs_dir.rglob("*.md"):
            resources.append(
                ResourceDescriptor(
                    uri=f"file:///{md_file}",
                    name=md_file.stem,
                    mimeType="text/markdown",
                    description=f"Documentation: {md_file.stem}"
                ).model_dump()
            )

    # Database schema resources
    db_schemas = await get_database_schemas()
    for schema in db_schemas:
        resources.append(
            ResourceDescriptor(
                uri=f"db:///schemas/{schema.name}",
                name=f"{schema.name} Schema",
                mimeType="application/json",
                description=f"Database schema for {schema.name}"
            ).model_dump()
        )

    return resources

read_resource Implementation

import aiofiles
from pathlib import Path

class ResourceContent(BaseModel):
    """Content returned by read_resource."""
    model_config = ConfigDict(strict=True)

    uri: str
    text: str | None = None
    blob: str | None = None  # Base64 encoded binary
    mimeType: str

@server.read_resource()
async def read_resource(uri: str) -> dict:
    """Read resource content by URI."""

    # Validate URI scheme
    if uri.startswith("file:///"):
        return await read_file_resource(uri)
    elif uri.startswith("db:///"):
        return await read_database_resource(uri)
    elif uri.startswith("log:///"):
        return await read_log_resource(uri)
    else:
        raise ValueError(f"Unsupported URI scheme: {uri}")

async def read_file_resource(uri: str) -> dict:
    """Read file-based resource."""
    # Security: validate path
    path = uri.removeprefix("file://")
    path = Path(path).resolve()

    # Prevent path traversal
    if ".." in str(path):
        raise ValueError("Path traversal not allowed")

    if not path.exists():
        raise FileNotFoundError(f"Resource not found: {uri}")

    # Async file reading
    async with aiofiles.open(path, "r") as f:
        content = await f.read()

    return {
        "contents": [
            ResourceContent(
                uri=uri,
                text=content,
                mimeType=get_mime_type(path)
            ).model_dump()
        ]
    }

Dynamic Resources

@server.list_resources()
async def list_resources() -> list[dict]:
    """List resources with dynamic content."""

    # Current date logs (past 7 days)
    resources = []
    for days_ago in range(7):
        date = datetime.now() - timedelta(days=days_ago)
        date_str = date.strftime("%Y-%m-%d")

        resources.append(
            ResourceDescriptor(
                uri=f"log:///app/{date_str}",
                name=f"App Logs - {date_str}",
                mimeType="text/plain",
                description=f"Application logs for {date_str}"
            ).model_dump()
        )

    return resources

@server.read_resource()
async def read_resource(uri: str) -> dict:
    """Read dynamic log resource."""
    if uri.startswith("log:///app/"):
        date_str = uri.removeprefix("log:///app/")
        log_content = await fetch_logs_for_date(date_str)

        return {
            "contents": [
                ResourceContent(
                    uri=uri,
                    text=log_content,
                    mimeType="text/plain"
                ).model_dump()
            ]
        }

MIME Type Detection

import mimetypes

def get_mime_type(path: Path) -> str:
    """Determine MIME type from file extension."""
    mime_type, _ = mimetypes.guess_type(str(path))

    # Fallback mapping
    if mime_type is None:
        extension_map = {
            ".md": "text/markdown",
            ".json": "application/json",
            ".yaml": "text/yaml",
            ".yml": "text/yaml",
            ".log": "text/plain",
            ".sql": "application/sql"
        }
        mime_type = extension_map.get(path.suffix, "text/plain")

    return mime_type

Security Validation

from pathlib import Path

class UriValidator:
    """Validate resource URIs for security."""

    @staticmethod
    def validate_file_uri(uri: str, allowed_roots: list[Path]) -> Path:
        """Validate file URI against allowed roots."""
        path = uri.removeprefix("file://")
        resolved = Path(path).resolve()

        # Check path traversal
        if ".." in str(path):
            raise ValueError("Path traversal not allowed")

        # Check against allowed roots
        if not any(resolved.is_relative_to(root) for root in allowed_roots):
            raise ValueError(f"Access denied: {uri}")

        return resolved

# Usage
@server.read_resource()
async def read_resource(uri: str) -> dict:
    if uri.startswith("file:///"):
        allowed_roots = [
            Path("/project/docs"),
            Path("/project/config")
        ]
        path = UriValidator.validate_file_uri(uri, allowed_roots)
        # Proceed with reading

Binary Resources

import base64

async def read_binary_resource(uri: str) -> dict:
    """Read binary resource (images, PDFs, etc.)."""
    path = Path(uri.removeprefix("file://"))

    async with aiofiles.open(path, "rb") as f:
        binary_content = await f.read()

    # Base64 encode binary data
    encoded = base64.b64encode(binary_content).decode("utf-8")

    return {
        "contents": [
            ResourceContent(
                uri=uri,
                blob=encoded,  # Use blob for binary
                mimeType=get_mime_type(path)
            ).model_dump()
        ]
    }

Database Schema Resources

@server.read_resource()
async def read_resource(uri: str) -> dict:
    """Read database schema resource."""
    if uri.startswith("db:///schemas/"):
        table_name = uri.removeprefix("db:///schemas/")

        # Fetch schema asynchronously
        schema = await fetch_table_schema(table_name)

        schema_json = {
            "table": table_name,
            "columns": [
                {
                    "name": col.name,
                    "type": col.type,
                    "nullable": col.nullable,
                    "primary_key": col.primary_key
                }
                for col in schema.columns
            ]
        }

        return {
            "contents": [
                ResourceContent(
                    uri=uri,
                    text=json.dumps(schema_json, indent=2),
                    mimeType="application/json"
                ).model_dump()
            ]
        }

Anti-Patterns

Using Tools for Read-Only Operations

# WRONG - should be a resource
@server.call_tool()
async def call_tool(name, args):
    if name == "read_docs":  # This should be a resource!
        return await read_documentation()

Missing URI Validation

# WRONG - security risk
path = uri.removeprefix("file://")
content = open(path).read()  # No validation!

Synchronous File I/O

# WRONG - blocking operation
with open(path, "r") as f:  # Should use aiofiles
    content = f.read()

Hardcoded Resource List

# WRONG - not dynamic
@server.list_resources()
async def list_resources():
    return [{"uri": "file:///README.md"}]  # Should scan filesystem

Resources (Tier 3)

MCP Resources Spec: https://spec.modelcontextprotocol.io/specification/server/resources/ aiofiles Docs: https://github.com/Tinche/aiofiles URI RFC: https://datatracker.ietf.org/doc/html/rfc3986