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 contentdict/ Pydantic model - serialized to JSONToolResult- full control over content type and embedded resourceslist[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.