mcp#

MCP (Model Context Protocol) lets you build tools that LLMs can use. fastmcp makes this straightforward.

what MCP is#

MCP servers expose:

  • tools - functions LLMs can call (actions, side effects)
  • resources - read-only data (like GET endpoints)
  • prompts - reusable message templates

clients (like Claude) discover and call these over stdio or HTTP.

basic server#

from fastmcp import FastMCP

mcp = FastMCP("my-server")

@mcp.tool
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

@mcp.resource("config://version")
def get_version() -> str:
    return "1.0.0"

if __name__ == "__main__":
    mcp.run()

fastmcp generates JSON schemas from type hints and docstrings automatically.

running#

# stdio (default, for local tools)
python server.py

# http (for deployment)
fastmcp run server.py --transport http --port 8000

tools vs resources#

tools do things:

@mcp.tool
async def create_post(text: str) -> dict:
    """Create a new post."""
    return await api.create(text)

resources read things:

@mcp.resource("posts://{post_id}")
async def get_post(post_id: str) -> dict:
    """Get a post by ID."""
    return await api.get(post_id)

context#

access MCP capabilities within tools:

from fastmcp import FastMCP, Context

mcp = FastMCP("server")

@mcp.tool
async def process(uri: str, ctx: Context) -> str:
    await ctx.info(f"Processing {uri}...")
    data = await ctx.read_resource(uri)
    await ctx.report_progress(50, 100)
    return data

middleware#

add authentication or other cross-cutting concerns:

from fastmcp import FastMCP
from fastmcp.server.middleware import Middleware

class AuthMiddleware(Middleware):
    async def on_call_tool(self, context, call_next):
        # extract auth from headers, set context state
        return await call_next(context)

mcp = FastMCP("server")
mcp.add_middleware(AuthMiddleware())

decorator patterns#

add parameters dynamically (from pdsx):

import inspect
from functools import wraps

def filterable(fn):
    """Add a _filter parameter for JMESPath filtering."""
    @wraps(fn)
    async def wrapper(*args, _filter: str | None = None, **kwargs):
        result = await fn(*args, **kwargs)
        if _filter:
            import jmespath
            return jmespath.search(_filter, result)
        return result

    # modify signature to include new param
    sig = inspect.signature(fn)
    params = list(sig.parameters.values())
    params.append(inspect.Parameter(
        "_filter",
        inspect.Parameter.KEYWORD_ONLY,
        default=None,
        annotation=str | None,
    ))
    wrapper.__signature__ = sig.replace(parameters=params)
    return wrapper

@mcp.tool
@filterable
async def list_records(collection: str) -> list[dict]:
    ...

response size protection#

LLMs have context limits. protect against flooding:

MAX_RESPONSE_CHARS = 30000

def truncate_response(records: list) -> list:
    import json
    serialized = json.dumps(records)
    if len(serialized) <= MAX_RESPONSE_CHARS:
        return records
    # truncate and add message about using _filter
    ...

claude code plugins#

structure for Claude Code integration:

.claude-plugin/
├── plugin.json          # plugin definition
└── marketplace.json     # marketplace metadata

skills/
└── domain/
    └── SKILL.md         # contextual guidance

plugin.json:

{
  "name": "myserver",
  "description": "what it does",
  "mcpServers": "./.mcp.json"
}

skills are markdown files loaded as context when relevant to the task.

entry points#

expose both CLI and MCP server:

[project.scripts]
mytool = "mytool.cli:main"
mytool-mcp = "mytool.mcp:main"

sources: