Skip to content

Commit 9327bcc

Browse files
committed
feat(fastmcp): add include_in_context parameter to resource decorator
Add a user-friendly include_in_context boolean parameter to the @resource decorator that automatically sets priority=1.0 in annotations, making it easier for users to mark resources for context inclusion without manually managing annotation objects. Changes: - Add include_in_context parameter to resource decorator - Automatically create Annotations(priority=1.0) when enabled - Override explicit priority when include_in_context=True - Preserve other annotation fields (audience) - Support both regular and template resources - Update docstring with new parameter documentation - Add comprehensive test coverage (6 new tests) The parameter provides a simple API for context inclusion: @mcp.resource("resource://data", include_in_context=True) def get_data(): return "important data"
1 parent b19fa6f commit 9327bcc

File tree

2 files changed

+107
-2
lines changed

2 files changed

+107
-2
lines changed

src/mcp/server/fastmcp/server.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@ def resource(
525525
mime_type: str | None = None,
526526
icons: list[Icon] | None = None,
527527
annotations: Annotations | None = None,
528+
include_in_context: bool = False,
528529
) -> Callable[[AnyFunction], AnyFunction]:
529530
"""Decorator to register a function as a resource.
530531
@@ -543,6 +544,9 @@ def resource(
543544
title: Optional human-readable title for the resource
544545
description: Optional description of the resource
545546
mime_type: Optional MIME type for the resource
547+
icons: Optional list of icons for the resource
548+
annotations: Optional annotations for the resource (audience, priority)
549+
include_in_context: If True, automatically sets priority to 1.0 for context inclusion
546550
547551
Example:
548552
@server.resource("resource://my-resource")
@@ -571,6 +575,15 @@ async def get_weather(city: str) -> str:
571575
)
572576

573577
def decorator(fn: AnyFunction) -> AnyFunction:
578+
# Handle include_in_context parameter
579+
processed_annotations = annotations
580+
if include_in_context:
581+
if processed_annotations is None:
582+
processed_annotations = Annotations(priority=1.0)
583+
else:
584+
# Override priority to 1.0, preserving other fields
585+
processed_annotations = Annotations(audience=processed_annotations.audience, priority=1.0)
586+
574587
# Check if this should be a template
575588
sig = inspect.signature(fn)
576589
has_uri_params = "{" in uri and "}" in uri
@@ -600,7 +613,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
600613
description=description,
601614
mime_type=mime_type,
602615
icons=icons,
603-
annotations=annotations,
616+
annotations=processed_annotations,
604617
)
605618
else:
606619
# Register as regular resource
@@ -612,7 +625,7 @@ def decorator(fn: AnyFunction) -> AnyFunction:
612625
description=description,
613626
mime_type=mime_type,
614627
icons=icons,
615-
annotations=annotations,
628+
annotations=processed_annotations,
616629
)
617630
self.add_resource(resource)
618631
return fn

tests/server/fastmcp/resources/test_resources.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,95 @@ def test_audience_validation(self):
193193
# Invalid roles should raise validation error
194194
with pytest.raises(Exception): # Pydantic validation error
195195
Annotations(audience=["invalid_role"]) # type: ignore
196+
197+
198+
class TestIncludeInContext:
199+
"""Test the include_in_context parameter."""
200+
201+
@pytest.mark.anyio
202+
async def test_include_in_context_sets_priority(self):
203+
"""Test that include_in_context=True sets priority to 1.0."""
204+
mcp = FastMCP()
205+
206+
@mcp.resource("resource://important", include_in_context=True)
207+
def get_important() -> str: # pragma: no cover
208+
return "important data"
209+
210+
resources = await mcp.list_resources()
211+
assert len(resources) == 1
212+
assert resources[0].annotations is not None
213+
assert resources[0].annotations.priority == 1.0
214+
215+
@pytest.mark.anyio
216+
async def test_include_in_context_false_no_priority(self):
217+
"""Test that include_in_context=False doesn't set priority."""
218+
mcp = FastMCP()
219+
220+
@mcp.resource("resource://normal", include_in_context=False)
221+
def get_normal() -> str: # pragma: no cover
222+
return "normal data"
223+
224+
resources = await mcp.list_resources()
225+
assert len(resources) == 1
226+
assert resources[0].annotations is None
227+
228+
@pytest.mark.anyio
229+
async def test_include_in_context_overrides_explicit_priority(self):
230+
"""Test that include_in_context=True overrides explicit priority."""
231+
mcp = FastMCP()
232+
233+
@mcp.resource("resource://override", include_in_context=True, annotations=Annotations(priority=0.3))
234+
def get_override() -> str: # pragma: no cover
235+
return "overridden"
236+
237+
resources = await mcp.list_resources()
238+
assert len(resources) == 1
239+
assert resources[0].annotations is not None
240+
assert resources[0].annotations.priority == 1.0
241+
242+
@pytest.mark.anyio
243+
async def test_include_in_context_preserves_audience(self):
244+
"""Test that include_in_context preserves existing audience."""
245+
mcp = FastMCP()
246+
247+
@mcp.resource(
248+
"resource://preserve",
249+
include_in_context=True,
250+
annotations=Annotations(audience=["user"], priority=0.5),
251+
)
252+
def get_preserve() -> str: # pragma: no cover
253+
return "preserved audience"
254+
255+
resources = await mcp.list_resources()
256+
assert len(resources) == 1
257+
assert resources[0].annotations is not None
258+
assert resources[0].annotations.priority == 1.0
259+
assert resources[0].annotations.audience == ["user"]
260+
261+
@pytest.mark.anyio
262+
async def test_include_in_context_with_template_resource(self):
263+
"""Test that include_in_context works with template resources."""
264+
mcp = FastMCP()
265+
266+
@mcp.resource("resource://{id}/data", include_in_context=True)
267+
def get_template_data(id: str) -> str: # pragma: no cover
268+
return f"data for {id}"
269+
270+
templates = await mcp.list_resource_templates()
271+
assert len(templates) == 1
272+
assert templates[0].annotations is not None
273+
assert templates[0].annotations.priority == 1.0
274+
275+
@pytest.mark.anyio
276+
async def test_include_in_context_with_async_function(self):
277+
"""Test that include_in_context works with async functions."""
278+
mcp = FastMCP()
279+
280+
@mcp.resource("resource://async", include_in_context=True)
281+
async def get_async() -> str: # pragma: no cover
282+
return "async data"
283+
284+
resources = await mcp.list_resources()
285+
assert len(resources) == 1
286+
assert resources[0].annotations is not None
287+
assert resources[0].annotations.priority == 1.0

0 commit comments

Comments
 (0)