claude/skills/mcp-patterns/SKILL.md
MCP (Model Context Protocol) server patterns — FastMCP server design, tool/resource/prompt definitions, authentication, testing, and deployment.
npx skillsauth add aleonsa/claude-config mcp-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Design and build production MCP servers that expose tools, resources, and prompts to AI assistants.
| Primitive | Purpose | Client sees | |-----------|---------|-------------| | Tool | Executable action the LLM can call (read DB, call API, run code) | Function with input schema | | Resource | Read-only data the LLM can access (file, DB row, API response) | URI-addressed content | | Prompt | Reusable prompt template with arguments | Rendered message list |
pip install fastmcp
# server.py
from fastmcp import FastMCP
mcp = FastMCP(
name="my-server",
version="1.0.0",
instructions="Tools for querying the product catalog.",
)
Tools are the most commonly used primitive. The LLM decides when to call them.
from fastmcp import FastMCP
from pydantic import BaseModel, Field
mcp = FastMCP("product-server")
# Simple tool — docstring becomes the description
@mcp.tool()
async def search_products(
query: str = Field(..., description="Search term"),
max_results: int = Field(10, ge=1, le=100, description="Max results to return"),
) -> list[dict]:
"""Search the product catalog by keyword. Returns matching products with name, price, and stock."""
results = await db.execute(
"SELECT id, name, price, stock FROM products WHERE name ILIKE $1 LIMIT $2",
f"%{query}%", max_results,
)
return [dict(r) for r in results]
@mcp.tool()
async def get_order_status(
order_id: str = Field(..., description="Order ID (e.g. ORD-12345)"),
) -> dict:
"""Get the current status and tracking info for an order."""
order = await orders_service.get(order_id)
if not order:
return {"error": f"Order {order_id} not found"}
return {"id": order.id, "status": order.status, "estimated_delivery": order.eta}
@mcp.tool()
async def create_refund(
order_id: str,
reason: str = Field(..., description="Reason for refund"),
amount: float | None = Field(None, description="Partial amount; omit for full refund"),
) -> dict:
"""Initiate a refund for an order. Requires order to be in delivered status."""
# Always validate before mutating state
order = await orders_service.get(order_id)
if not order:
raise ValueError(f"Order {order_id} not found")
if order.status != "delivered":
raise ValueError(f"Can only refund delivered orders, got: {order.status}")
return await refunds_service.create(order_id, reason, amount)
{"error": "..."} for expected failures; raise for unexpectedconfirm: bool paramaction flagsResources expose read-only data as URI-addressable content.
from fastmcp import FastMCP
from fastmcp.resources import Resource
mcp = FastMCP("docs-server")
# Static resource
@mcp.resource("docs://readme")
async def get_readme() -> str:
"""Main project README."""
return Path("README.md").read_text()
# Dynamic resource with URI template
@mcp.resource("users://{user_id}/profile")
async def get_user_profile(user_id: int) -> dict:
"""User profile data. URI: users://<id>/profile"""
user = await users_service.get(user_id)
if not user:
raise ValueError(f"User {user_id} not found")
return {"id": user.id, "email": user.email, "name": user.name}
@mcp.resource("reports://{date}/summary")
async def get_daily_report(date: str) -> str:
"""Daily summary report. Date format: YYYY-MM-DD"""
report = await reports_service.get_by_date(date)
return report.markdown_content
scheme://path format: docs://, db://, files://{braces} map to function argumentsstr for text, bytes for binary, dict for JSONPrompts are reusable message templates with typed arguments.
from fastmcp import FastMCP
from mcp.types import PromptMessage, TextContent
mcp = FastMCP("prompts-server")
@mcp.prompt()
def code_review_prompt(
language: str,
code: str,
focus: str = "general quality, security, and performance",
) -> list[PromptMessage]:
"""Generate a code review prompt for a given language and code snippet."""
return [
PromptMessage(
role="user",
content=TextContent(
type="text",
text=f"""Review the following {language} code.
Focus on: {focus}
```{language}
{code}
Provide specific, actionable feedback.""", ), ) ]
@mcp.prompt() def summarize_document( document: str, format: str = "bullet points", max_length: str = "200 words", ) -> list[PromptMessage]: """Summarize a document in the specified format and length.""" return [ PromptMessage( role="user", content=TextContent( type="text", text=f"Summarize the following document in {format}, max {max_length}:\n\n{document}", ), ) ]
## Authentication
```python
# Option 1: API key via header (simple, for internal tools)
from fastmcp import FastMCP
from fastmcp.server.auth import BearerAuthProvider
auth = BearerAuthProvider(
token=settings.mcp_api_key.get_secret_value(),
)
mcp = FastMCP("secure-server", auth=auth)
# Option 2: Custom auth (per-request validation)
from fastmcp.server.auth import AuthProvider
from starlette.requests import Request
class CustomAuthProvider(AuthProvider):
async def authenticate(self, request: Request) -> bool:
token = request.headers.get("Authorization", "").removeprefix("Bearer ")
return await validate_token(token) # your logic
mcp = FastMCP("secure-server", auth=CustomAuthProvider())
from contextlib import asynccontextmanager
from fastmcp import FastMCP
from sqlalchemy.ext.asyncio import AsyncSession
# Inject DB session into tools via lifespan
@asynccontextmanager
async def lifespan(server: FastMCP):
async with AsyncSessionLocal() as session:
server.state.db = session
yield
await session.close()
mcp = FastMCP("db-server", lifespan=lifespan)
@mcp.tool()
async def get_user(
user_id: int,
ctx: Context, # FastMCP injects this automatically
) -> dict:
"""Get a user by ID."""
db: AsyncSession = ctx.server.state.db
user = await db.get(User, user_id)
return {"id": user.id, "email": user.email} if user else {"error": "Not found"}
# app/main.py
from fastapi import FastAPI
from fastmcp import FastMCP
from app.mcp_server import mcp
app = FastAPI()
# Mount MCP server as a sub-application
app.mount("/mcp", mcp.get_asgi_app())
# Regular FastAPI routes
@app.get("/health")
async def health():
return {"status": "ok"}
myproject/
├── app/
│ ├── main.py
│ └── ...
├── mcp/
│ ├── __init__.py
│ ├── server.py # FastMCP instance + lifespan
│ ├── tools/
│ │ ├── __init__.py
│ │ ├── products.py # product-related tools
│ │ ├── orders.py # order-related tools
│ │ └── users.py
│ ├── resources/
│ │ ├── __init__.py
│ │ └── docs.py
│ └── prompts/
│ ├── __init__.py
│ └── templates.py
└── tests/
└── test_mcp/
├── test_tools.py
└── test_resources.py
# tests/test_mcp/test_tools.py
import pytest
from fastmcp import FastMCP, Client
@pytest.fixture
def mcp_server() -> FastMCP:
from mcp.server import mcp # your server instance
return mcp
@pytest.fixture
async def client(mcp_server: FastMCP) -> Client:
async with Client(mcp_server) as c:
yield c
async def test_search_products_returns_results(client: Client, db):
result = await client.call_tool("search_products", {"query": "laptop"})
assert isinstance(result, list)
assert len(result) > 0
assert "name" in result[0]
async def test_search_products_empty_query(client: Client):
result = await client.call_tool("search_products", {"query": ""})
assert isinstance(result, list) # graceful, not an error
async def test_get_order_status_not_found(client: Client):
result = await client.call_tool("get_order_status", {"order_id": "ORD-99999"})
assert "error" in result
async def test_list_tools(client: Client):
tools = await client.list_tools()
tool_names = {t.name for t in tools}
assert "search_products" in tool_names
assert "get_order_status" in tool_names
async def test_get_readme_resource(client: Client):
content = await client.read_resource("docs://readme")
assert len(content) > 0
async def test_code_review_prompt(client: Client):
messages = await client.get_prompt(
"code_review_prompt",
{"language": "python", "code": "def foo(): pass"},
)
assert len(messages) == 1
assert "python" in messages[0].content.text
# server.py
if __name__ == "__main__":
mcp.run() # uses stdio transport by default
// claude_desktop_config.json
{
"mcpServers": {
"my-server": {
"command": "uv",
"args": ["run", "python", "server.py"],
"env": {"DATABASE_URL": "postgresql://..."}
}
}
}
# server.py
if __name__ == "__main__":
mcp.run(transport="sse", host="0.0.0.0", port=8080)
FROM python:3.12-slim
WORKDIR /app
COPY pyproject.toml .
RUN pip install uv && uv sync --frozen
COPY . .
EXPOSE 8080
CMD ["uv", "run", "python", "server.py"]
| Anti-pattern | Problem | Fix |
|--------------|---------|-----|
| Vague tool description | LLM calls wrong tool or skips it | Write specific, usage-oriented docstrings |
| Raising exceptions for expected failures | Crashes the agent loop | Return {"error": "..."} for expected cases |
| Mutable state in tools | Race conditions under concurrent calls | Keep tools stateless; use injected DB sessions |
| Tools that do too much | LLM can't compose them | One responsibility per tool |
| No auth on HTTP server | Anyone can call your tools | Add BearerAuthProvider |
| Returning raw text from tools | Hard for LLM to parse | Return structured dicts |
| Skipping tests | Breakage only found in production | Use Client fixture for all tools |
| Task | Pattern |
|------|---------|
| Define a tool | @mcp.tool() async function with Field descriptions |
| Define a resource | @mcp.resource("scheme://path") |
| Define a prompt | @mcp.prompt() returning list[PromptMessage] |
| Inject DB session | lifespan + ctx.server.state.db |
| Add auth | BearerAuthProvider(token=...) |
| Embed in FastAPI | app.mount("/mcp", mcp.get_asgi_app()) |
| Test tools | async with Client(mcp_server) as client |
| Run standalone | mcp.run() (stdio) or mcp.run(transport="sse") |
development
CUDA kernel development and GPU optimization patterns — memory hierarchy, occupancy tuning, coalescing, shared memory tiling, warp-level ops, and profiling with Nsight Compute. Use when writing or optimizing CUDA C++ kernels.
documentation
Translate visa application documents (images) to English and create a bilingual PDF with original and translation
development
A comprehensive verification system for Claude Code sessions.
development
Use this skill when writing new features, fixing bugs, or refactoring code. Enforces test-driven development with 80%+ coverage including unit, integration, and E2E tests.