Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 115 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,9 @@ Resources are how you expose data to LLMs. They're similar to GET endpoints in a

<!-- snippet-source examples/snippets/servers/basic_resource.py -->
```python
from mcp.server.fastmcp import FastMCP
from typing import Annotated

from mcp.server.fastmcp import FastMCP, Path, Query

mcp = FastMCP(name="Resource Example")

Expand All @@ -289,6 +291,118 @@ def get_settings() -> str:
"language": "en",
"debug": false
}"""


# Form-style query expansion examples using RFC 6570 URI templates


@mcp.resource("articles://{article_id}/view")
def view_article(article_id: str, format: str = "html", lang: str = "en") -> str:
"""View an article with optional format and language selection.

Example URIs:
- articles://123/view (uses defaults: format=html, lang=en)
- articles://123/view?format=pdf (format=pdf, lang=en)
- articles://123/view?format=pdf&lang=fr (format=pdf, lang=fr)
"""
if format == "pdf":
content = f"PDF content for article {article_id} in {lang}"
elif format == "json":
content = f'{{"article_id": "{article_id}", "content": "...", "lang": "{lang}"}}'
else:
content = f"<html><body>Article {article_id} in {lang}</body></html>"

return content


@mcp.resource("search://query/{search_term}")
def search_content(
search_term: str, page: int = 1, limit: int = 10, category: str = "all", sort: str = "relevance"
) -> str:
"""Search content with optional pagination and filtering.

Example URIs:
- search://query/python (basic search)
- search://query/python?page=2&limit=20 (pagination)
- search://query/python?category=tutorial&sort=date (filtering)
"""
offset = (page - 1) * limit
results = f"Search results for '{search_term}' (category: {category}, sort: {sort})"
results += f"\nShowing {limit} results starting from {offset + 1}"

# Simulated search results
for i in range(limit):
result_num = offset + i + 1
results += f"\n{result_num}. Result about {search_term} in {category}"

return results


@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: str, include_private: bool = False, format: str = "summary") -> str:
"""Get user profile with optional private data and format selection.

Example URIs:
- users://123/profile (public data, summary format)
- users://123/profile?include_private=true (includes private data)
- users://123/profile?format=detailed&include_private=true (detailed with private)
"""
from typing import Any

profile_data: dict[str, Any] = {"user_id": user_id, "name": "John Doe", "public_bio": "Software developer"}

if include_private:
profile_data.update({"email": "john@example.com", "phone": "+1234567890"})

if format == "detailed":
profile_data.update({"last_active": "2024-01-20", "preferences": {"notifications": True}})

return str(profile_data)


@mcp.resource("api://weather/{location}")
def get_weather_data(
location: str, units: str = "metric", lang: str = "en", include_forecast: bool = False, days: int = 5
) -> str:
"""Get weather data with customizable options.

Example URIs:
- api://weather/london (basic weather)
- api://weather/london?units=imperial&lang=es (different units and language)
- api://weather/london?include_forecast=true&days=7 (with 7-day forecast)
"""
temp_unit = "C" if units == "metric" else "F"
base_temp = 22 if units == "metric" else 72

weather_info = f"Weather for {location}: {base_temp}{temp_unit}"

if include_forecast:
weather_info += f"\n{days}-day forecast:"
for day in range(1, days + 1):
forecast_temp = base_temp + (day % 3)
weather_info += f"\nDay {day}: {forecast_temp}{temp_unit}"

return weather_info


@mcp.resource("api://data/{user_id}/{region}/{city}/{file_path:path}")
def resource_fn(
# Path parameters
user_id: Annotated[int, Path(gt=0, description="User ID")], # explicit Path
region, # inferred path # type: ignore
city: str, # inferred path
file_path: str, # inferred path {file_path:path}
# Required query parameter (no default)
version: int,
# Optional query parameters (defaults or Query(...))
format: Annotated[str, Query("json", description="Output format")],
include_metadata: bool = False,
tags: list[str] = [],
lang: str = "en",
debug: bool = False,
precision: float = 0.5,
) -> str:
return f"{user_id}/{region}/{city}/{file_path}"
```

_Full example: [examples/snippets/servers/basic_resource.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_resource.py)_
Expand Down
116 changes: 115 additions & 1 deletion examples/snippets/servers/basic_resource.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from mcp.server.fastmcp import FastMCP
from typing import Annotated

from mcp.server.fastmcp import FastMCP, Path, Query

mcp = FastMCP(name="Resource Example")

Expand All @@ -18,3 +20,115 @@ def get_settings() -> str:
"language": "en",
"debug": false
}"""


# Form-style query expansion examples using RFC 6570 URI templates


@mcp.resource("articles://{article_id}/view")
def view_article(article_id: str, format: str = "html", lang: str = "en") -> str:
"""View an article with optional format and language selection.

Example URIs:
- articles://123/view (uses defaults: format=html, lang=en)
- articles://123/view?format=pdf (format=pdf, lang=en)
- articles://123/view?format=pdf&lang=fr (format=pdf, lang=fr)
"""
if format == "pdf":
content = f"PDF content for article {article_id} in {lang}"
elif format == "json":
content = f'{{"article_id": "{article_id}", "content": "...", "lang": "{lang}"}}'
else:
content = f"<html><body>Article {article_id} in {lang}</body></html>"

return content


@mcp.resource("search://query/{search_term}")
def search_content(
search_term: str, page: int = 1, limit: int = 10, category: str = "all", sort: str = "relevance"
) -> str:
"""Search content with optional pagination and filtering.

Example URIs:
- search://query/python (basic search)
- search://query/python?page=2&limit=20 (pagination)
- search://query/python?category=tutorial&sort=date (filtering)
"""
offset = (page - 1) * limit
results = f"Search results for '{search_term}' (category: {category}, sort: {sort})"
results += f"\nShowing {limit} results starting from {offset + 1}"

# Simulated search results
for i in range(limit):
result_num = offset + i + 1
results += f"\n{result_num}. Result about {search_term} in {category}"

return results


@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: str, include_private: bool = False, format: str = "summary") -> str:
"""Get user profile with optional private data and format selection.

Example URIs:
- users://123/profile (public data, summary format)
- users://123/profile?include_private=true (includes private data)
- users://123/profile?format=detailed&include_private=true (detailed with private)
"""
from typing import Any

profile_data: dict[str, Any] = {"user_id": user_id, "name": "John Doe", "public_bio": "Software developer"}

if include_private:
profile_data.update({"email": "john@example.com", "phone": "+1234567890"})

if format == "detailed":
profile_data.update({"last_active": "2024-01-20", "preferences": {"notifications": True}})

return str(profile_data)


@mcp.resource("api://weather/{location}")
def get_weather_data(
location: str, units: str = "metric", lang: str = "en", include_forecast: bool = False, days: int = 5
) -> str:
"""Get weather data with customizable options.

Example URIs:
- api://weather/london (basic weather)
- api://weather/london?units=imperial&lang=es (different units and language)
- api://weather/london?include_forecast=true&days=7 (with 7-day forecast)
"""
temp_unit = "C" if units == "metric" else "F"
base_temp = 22 if units == "metric" else 72

weather_info = f"Weather for {location}: {base_temp}{temp_unit}"

if include_forecast:
weather_info += f"\n{days}-day forecast:"
for day in range(1, days + 1):
forecast_temp = base_temp + (day % 3)
weather_info += f"\nDay {day}: {forecast_temp}{temp_unit}"

return weather_info


@mcp.resource("api://data/{user_id}/{region}/{city}/{file_path:path}")
def resource_fn(
# Path parameters
user_id: Annotated[int, Path(gt=0, description="User ID")], # explicit Path
region, # inferred path # type: ignore
city: str, # inferred path
file_path: str, # inferred path {file_path:path}
# Required query parameter (no default)
version: int,
# Optional query parameters (defaults or Query(...))
format: Annotated[str, Query("json", description="Output format")],
include_metadata: bool = False,
tags: list[str] = [],
lang: str = "en",
debug: bool = False,
precision: float = 0.5,
) -> str:
return f"{user_id}/{region}/{city}/{file_path}"
3 changes: 2 additions & 1 deletion src/mcp/server/fastmcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from mcp.types import Icon

from .server import Context, FastMCP
from .utilities.param_functions import Path, Query
from .utilities.types import Audio, Image

__version__ = version("mcp")
__all__ = ["FastMCP", "Context", "Image", "Audio", "Icon"]
__all__ = ["FastMCP", "Context", "Image", "Audio", "Icon", "Path", "Query"]
Loading
Loading