Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
79bc4b8
Added RFC 6570 complaint form style query expansion as optional param…
Apr 4, 2025
33fd246
uv lock fix
Apr 4, 2025
01025ea
fix
Apr 5, 2025
397569a
fix mismatch
Apr 5, 2025
543e86c
added type validation for optional params
May 28, 2025
bfcbf6f
resolve conflicts
May 28, 2025
b076a13
fix format
May 28, 2025
7af828a
resolved conflicts
Jun 23, 2025
2ed668c
resolved conflicts
Sep 6, 2025
5d43128
Feature Added:
beaterblank Oct 7, 2025
8fb7988
linting and formatting
beaterblank Oct 7, 2025
8742101
Enhancement: compile and store the pattern once instead of recompilin…
beaterblank Oct 9, 2025
15d6426
formatting
beaterblank Oct 9, 2025
d1de7f4
Merge branch 'main' into main
beaterblank Oct 13, 2025
8e000d4
Merge branch 'main' into main
beaterblank Oct 14, 2025
2be0e2a
Merge branch 'main' into main
beaterblank Oct 16, 2025
4c10e65
Merge branch 'main' into main
beaterblank Oct 17, 2025
20b4d28
merge #427
beaterblank Oct 17, 2025
bc7df53
handle validation in templates
beaterblank Oct 17, 2025
d7c21c1
bug fix
beaterblank Oct 17, 2025
010c5f7
linting
beaterblank Oct 17, 2025
7d60f21
Merge branch 'main' into main
beaterblank Oct 17, 2025
6ab862b
added Path and Query Annotations
beaterblank Oct 18, 2025
7e3b415
Merge branch 'main' of https://github.com/beaterblank/python-sdk
beaterblank Oct 18, 2025
4027141
linting
beaterblank Oct 18, 2025
5873bdd
examples
beaterblank Oct 18, 2025
6c7819a
Path and Query bug fixes and tests and example.
beaterblank Oct 18, 2025
0953fcb
Merge branch 'main' into main
beaterblank Oct 25, 2025
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": "[email protected]", "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": "[email protected]", "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