Tools, Resources, Prompts

The three MCP primitives. FastMCP wraps each with decorators and auto-generates JSON schemas from type hints.

Tools

Tools are functions the LLM can call. Most common primitive.

from fastmcp import FastMCP

mcp = FastMCP("Demo")

# Minimal - description from docstring, schema from type hints
@mcp.tool
def search(query: str, limit: int = 10) -> list[dict]:
    """Search the database."""
    return db.search(query, limit=limit)

# With metadata
@mcp.tool(
    name="custom_name",
    description="Override the docstring",
    tags={"database", "read"},
    timeout=30.0,
)
def search_v2(query: str) -> list[dict]:
    return db.search(query)

Tool Annotations

MCP spec annotations hint to clients about tool behavior:

from mcp.types import ToolAnnotations

@mcp.tool(
    annotations=ToolAnnotations(
        title="Database Search",
        readOnlyHint=True,        # doesn't modify state
        destructiveHint=False,    # safe to run
        idempotentHint=True,      # same input = same output
        openWorldHint=False,      # no external network calls
    )
)
def search(query: str) -> list[dict]:
    """Search the local database."""
    return db.search(query)

Return Types

  • str - returned as text content
  • dict / Pydantic model - serialized to JSON
  • ToolResult - full control over content type and embedded resources
  • list[Content] - multiple content blocks (text, image, etc.)
from fastmcp.tools.tool import ToolResult

@mcp.tool
def get_image(path: str) -> ToolResult:
    data = open(path, "rb").read()
    return ToolResult(
        content=[ImageContent(data=base64.b64encode(data).decode(), mimeType="image/png")],
        isError=False,
    )

Output Schemas

FastMCP v2 supports output schemas (structured output from tools):

@mcp.tool(output_schema={"type": "object", "properties": {"count": {"type": "integer"}}})
def count_items(category: str) -> dict:
    return {"count": len(items[category])}

Async and Sync

Both work. Sync functions run in a thread pool automatically:

@mcp.tool
def sync_tool(x: int) -> int:      # runs in threadpool
    return x * 2

@mcp.tool
async def async_tool(x: int) -> int:  # native async
    return x * 2

Context Injection

Any parameter typed as Context (or named context) gets the request context injected:

from fastmcp import Context

@mcp.tool
async def smart_tool(query: str, context: Context) -> str:
    await context.send_message("info", f"Processing: {query}")
    return "done"

Resources

Resources expose data the LLM can read. Identified by URI.

# Static resource
@mcp.resource("config://app/settings")
def get_settings() -> dict:
    return {"debug": True, "version": "1.0"}

# Resource template with parameters
@mcp.resource("users://{user_id}/profile")
def get_profile(user_id: str) -> dict:
    return db.get_user(user_id)

URI Templates (RFC 6570)

# Simple parameter
@mcp.resource("file:///{path}")

# Wildcard (multi-segment path)
@mcp.resource("docs://{path*}")

# Query parameters
@mcp.resource("search://{?query,limit}")

Resource Content

from fastmcp.resources.resource import ResourceResult

@mcp.resource("data://report")
def report() -> ResourceResult:
    return ResourceResult(
        content="# Report\n...",
        mime_type="text/markdown",
    )

Prompts

Prompts are reusable message templates for LLMs.

from fastmcp.prompts.prompt import Message, PromptResult

@mcp.prompt()
def code_review(code: str, language: str = "python") -> PromptResult:
    return PromptResult([
        Message("You are a senior code reviewer.", role="assistant"),
        Message(f"Review this {language} code:\n\n```{language}\n{code}\n```", role="user"),
    ])

# Simple string return (becomes single user message)
@mcp.prompt()
def summarize(text: str) -> str:
    return f"Summarize the following text:\n\n{text}"

Programmatic Registration

All three primitives can be added without decorators:

def my_tool(x: int) -> int:
    return x * 2

mcp.add_tool(my_tool, name="double", description="Double a number")

# Resources and prompts similarly
mcp.add_resource(my_resource_fn, uri="scheme://path")
mcp.add_prompt(my_prompt_fn, name="reviewer")

Tags

Tools, resources, and prompts support tags for filtering and organization:

@mcp.tool(tags={"database", "read"})
def query_db(sql: str) -> list[dict]: ...

@mcp.tool(tags={"database", "write"})
def insert_record(table: str, data: dict) -> bool: ...

Clients can filter by tag when listing available tools.