Add task_meta parameter to read_resource() for explicit task control (#2750)

This commit is contained in:
Jeremiah Lowin 2025-12-26 12:59:01 -05:00 committed by GitHub
commit fc19f1f8de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 674 additions and 172 deletions

View file

@ -5,7 +5,8 @@ from __future__ import annotations
import base64
import inspect
from collections.abc import Callable
from typing import TYPE_CHECKING, Annotated, Any, ClassVar
from dataclasses import replace
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, overload
import mcp.types
@ -27,7 +28,7 @@ from pydantic import (
from typing_extensions import Self
from fastmcp.server.dependencies import without_injected_parameters
from fastmcp.server.tasks.config import TaskConfig
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.types import get_fn_name
@ -300,23 +301,42 @@ class Resource(FastMCPComponent):
# ResourceResult.__init__ handles all normalization
return ResourceResult(raw_value)
async def _read(self) -> ResourceResult | mcp.types.CreateTaskResult:
@overload
async def _read(self, task_meta: None = None) -> ResourceResult: ...
@overload
async def _read(self, task_meta: TaskMeta) -> mcp.types.CreateTaskResult: ...
async def _read(
self, task_meta: TaskMeta | None = None
) -> ResourceResult | mcp.types.CreateTaskResult:
"""Server entry point that handles task routing.
This allows ANY Resource subclass to support background execution by setting
task_config.mode to "supported" or "required". The server calls this
method instead of read() directly.
Args:
task_meta: If provided, execute as a background task and return
CreateTaskResult. If None (default), execute synchronously and
return ResourceResult.
Returns:
ResourceResult when task_meta is None.
CreateTaskResult when task_meta is provided.
Subclasses can override this to customize task routing behavior.
For example, FastMCPProviderResource overrides to delegate to child
middleware without submitting to Docket.
"""
from fastmcp.server.dependencies import _docket_fn_key
from fastmcp.server.tasks.routing import check_background_task
key = _docket_fn_key.get() or self.key
# Enrich task_meta with fn_key if not already set (fallback for programmatic API)
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=self.key)
task_result = await check_background_task(
component=self, task_type="resource", key=key
component=self, task_type="resource", arguments=None, task_meta=task_meta
)
if task_result:
return task_result

View file

@ -5,7 +5,8 @@ from __future__ import annotations
import inspect
import re
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, ClassVar
from dataclasses import replace
from typing import TYPE_CHECKING, Any, ClassVar, overload
from urllib.parse import parse_qs, unquote
import mcp.types
@ -23,7 +24,7 @@ from pydantic import (
from fastmcp.resources.resource import Resource, ResourceResult
from fastmcp.server.dependencies import without_injected_parameters
from fastmcp.server.tasks.config import TaskConfig
from fastmcp.server.tasks.config import TaskConfig, TaskMeta
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.json_schema import compress_schema
from fastmcp.utilities.types import get_cached_typeadapter
@ -177,8 +178,18 @@ class ResourceTemplate(FastMCPComponent):
# ResourceResult.__init__ handles all normalization
return ResourceResult(raw_value)
@overload
async def _read(
self, uri: str, params: dict[str, Any]
self, uri: str, params: dict[str, Any], task_meta: None = None
) -> ResourceResult: ...
@overload
async def _read(
self, uri: str, params: dict[str, Any], task_meta: TaskMeta
) -> mcp.types.CreateTaskResult: ...
async def _read(
self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None
) -> ResourceResult | mcp.types.CreateTaskResult:
"""Server entry point that handles task routing.
@ -186,19 +197,29 @@ class ResourceTemplate(FastMCPComponent):
by setting task_config.mode to "supported" or "required". The server calls
this method instead of create_resource()/read() directly.
Args:
uri: The concrete URI being read
params: Template parameters extracted from the URI
task_meta: If provided, execute as a background task and return
CreateTaskResult. If None (default), execute synchronously and
return ResourceResult.
Returns:
ResourceResult when task_meta is None.
CreateTaskResult when task_meta is provided.
Subclasses can override this to customize task routing behavior.
For example, FastMCPProviderResourceTemplate overrides to delegate to child
middleware without submitting to Docket.
"""
from fastmcp.server.dependencies import _docket_fn_key
from fastmcp.server.tasks.routing import check_background_task
# Templates need pattern check: only use contextvar if it contains '{'
key = _docket_fn_key.get()
if not key or "{" not in key:
key = self.key
# Enrich task_meta with fn_key if not already set (fallback for programmatic API)
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=self.key)
task_result = await check_background_task(
component=self, task_type="template", key=key, arguments=params
component=self, task_type="template", arguments=params, task_meta=task_meta
)
if task_result:
return task_result
@ -294,23 +315,43 @@ class FunctionResourceTemplate(ResourceTemplate):
fn: Callable[..., Any]
@overload
async def _read(
self, uri: str, params: dict[str, Any]
self, uri: str, params: dict[str, Any], task_meta: None = None
) -> ResourceResult: ...
@overload
async def _read(
self, uri: str, params: dict[str, Any], task_meta: TaskMeta
) -> mcp.types.CreateTaskResult: ...
async def _read(
self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None
) -> ResourceResult | mcp.types.CreateTaskResult:
"""Optimized server entry point that skips ephemeral resource creation.
For FunctionResourceTemplate, we can call read() directly instead of
creating a temporary resource, which is more efficient.
Args:
uri: The concrete URI being read
params: Template parameters extracted from the URI
task_meta: If provided, execute as a background task and return
CreateTaskResult. If None (default), execute synchronously and
return ResourceResult.
Returns:
ResourceResult when task_meta is None.
CreateTaskResult when task_meta is provided.
"""
from fastmcp.server.dependencies import _docket_fn_key
from fastmcp.server.tasks.routing import check_background_task
# Templates need pattern check: only use contextvar if it contains '{'
key = _docket_fn_key.get()
if not key or "{" not in key:
key = self.key
# Enrich task_meta with fn_key if not already set (fallback for programmatic API)
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=self.key)
task_result = await check_background_task(
component=self, task_type="template", key=key, arguments=params
component=self, task_type="template", arguments=params, task_meta=task_meta
)
if task_result:
return task_result

View file

@ -13,7 +13,8 @@ from __future__ import annotations
import re
from collections.abc import AsyncIterator, Sequence
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Any
from dataclasses import replace
from typing import TYPE_CHECKING, Any, overload
import mcp.types
from mcp.types import AnyUrl
@ -85,6 +86,20 @@ class FastMCPProviderTool(Tool):
task_config=tool.task_config,
)
@overload
async def _run(
self,
arguments: dict[str, Any],
task_meta: None = None,
) -> ToolResult: ...
@overload
async def _run(
self,
arguments: dict[str, Any],
task_meta: TaskMeta,
) -> mcp.types.CreateTaskResult: ...
async def _run(
self,
arguments: dict[str, Any],
@ -94,7 +109,15 @@ class FastMCPProviderTool(Tool):
Passes task_meta through to the child server so it can handle
backgrounding appropriately.
Enriches fn_key with self.key (the parent's namespaced tool name) so that
when the child tool delegates to Docket, it uses the correct lookup key
that was registered via get_tasks().
"""
# Enrich fn_key with parent's key before delegating to child
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=self.key)
return await self._server.call_tool(
self._original_name, arguments, task_meta=task_meta
)
@ -148,19 +171,29 @@ class FastMCPProviderResource(Resource):
task_config=resource.task_config,
)
async def _read(self) -> ResourceResult | mcp.types.CreateTaskResult:
"""Skip task routing - delegate to child server's read_resource().
@overload
async def _read(self, task_meta: None = None) -> ResourceResult: ...
The actual underlying resource will check _task_metadata contextvar and
submit to Docket if appropriate. This wrapper just passes through.
@overload
async def _read(self, task_meta: TaskMeta) -> mcp.types.CreateTaskResult: ...
Note: The _docket_fn_key contextvar is intentionally NOT updated here.
The parent set it to the full namespaced key (e.g., data://c/gc/value)
which is what the function is registered under in Docket. All provider
layers pass this through unchanged so the eventual resource._read()
uses the correct Docket lookup key.
async def _read(
self, task_meta: TaskMeta | None = None
) -> ResourceResult | mcp.types.CreateTaskResult:
"""Delegate to child server's read_resource() with task_meta.
Passes task_meta through to the child server so it can handle
backgrounding appropriately.
Enriches fn_key with self.key (the parent's namespaced URI) so that
when the child resource delegates to Docket, it uses the correct
lookup key that was registered via get_tasks().
"""
return await self._server.read_resource(self._original_uri)
# Enrich fn_key with parent's URI before delegating to child
if task_meta is not None and task_meta.fn_key is None:
task_meta = TaskMeta(ttl=task_meta.ttl, fn_key=self.key)
return await self._server.read_resource(self._original_uri, task_meta=task_meta)
class FastMCPProviderPrompt(Prompt):
@ -277,41 +310,36 @@ class FastMCPProviderResourceTemplate(ResourceTemplate):
mime_type=self.mime_type,
)
@overload
async def _read(
self, uri: str, params: dict[str, Any]
self, uri: str, params: dict[str, Any], task_meta: None = None
) -> ResourceResult: ...
@overload
async def _read(
self, uri: str, params: dict[str, Any], task_meta: TaskMeta
) -> mcp.types.CreateTaskResult: ...
async def _read(
self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None
) -> ResourceResult | mcp.types.CreateTaskResult:
"""Delegate to child server's read_resource().
"""Delegate to child server's read_resource() with task_meta.
Skips task routing at this layer - the child's template._read() will
check _task_metadata contextvar and submit to Docket if appropriate.
Passes task_meta through to the child server so it can handle
backgrounding appropriately.
Sets _docket_fn_key to self.uri_template (the transformed pattern) so that
when the child template's _read() submits to Docket, it uses the correct
key that matches what was registered via TransformingProvider.get_tasks().
Only sets _docket_fn_key if not already set - in nested mounts, the
outermost wrapper sets the key and inner wrappers preserve it.
Enriches fn_key with self.key (the parent's namespaced template pattern)
so that when the child template delegates to Docket, it uses the correct
lookup key that was registered via get_tasks().
"""
from fastmcp.server.dependencies import _docket_fn_key
# Expand the original template with params to get internal URI
original_uri = _expand_uri_template(self._original_uri_template or "", params)
# Set _docket_fn_key to the template pattern, but only if the current
# value isn't already a template pattern (contains '{').
# - Server sets concrete URI (e.g., "item://c/gc/42") - no '{', override it
# - Outer wrapper sets pattern (e.g., "item://c/gc/{id}") - has '{', keep it
# In nested mounts (parent→child→grandchild), the outermost wrapper
# has the fully-transformed pattern that matches Docket registration.
existing_key = _docket_fn_key.get()
key_token = None
if not existing_key or "{" not in existing_key:
key_token = _docket_fn_key.set(self.key)
try:
return await self._server.read_resource(original_uri)
finally:
if key_token is not None:
_docket_fn_key.reset(key_token)
# Enrich fn_key with parent's namespaced template pattern before delegating
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=self.key)
return await self._server.read_resource(original_uri, task_meta=task_meta)
async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:
"""Read the resource content for background task execution.

View file

@ -1176,9 +1176,8 @@ class FastMCP(Generic[LifespanResultT]):
ToolError: If tool execution fails
ValidationError: If arguments fail validation
"""
# Enrich task_meta with fn_key if task execution requested
if task_meta is not None and task_meta.fn_key is None:
task_meta = TaskMeta(ttl=task_meta.ttl, fn_key=Tool.make_key(name))
# Note: fn_key enrichment happens in Tool._run(), not here,
# so that provider wrappers can enrich with their namespaced key.
async with fastmcp.server.context.Context(fastmcp=self) as ctx:
if run_middleware:
@ -1221,11 +1220,30 @@ class FastMCP(Generic[LifespanResultT]):
raise NotFoundError(f"Unknown tool: {name!r}")
@overload
async def read_resource(
self,
uri: str,
*,
run_middleware: bool = True,
task_meta: None = None,
) -> ResourceResult: ...
@overload
async def read_resource(
self,
uri: str,
*,
run_middleware: bool = True,
task_meta: TaskMeta,
) -> mcp.types.CreateTaskResult: ...
async def read_resource(
self,
uri: str,
*,
run_middleware: bool = True,
task_meta: TaskMeta | None = None,
) -> ResourceResult | mcp.types.CreateTaskResult:
"""Read a resource by URI.
@ -1236,15 +1254,23 @@ class FastMCP(Generic[LifespanResultT]):
uri: The resource URI
run_middleware: If True (default), apply the middleware chain.
Set to False when called from middleware to avoid re-applying.
task_meta: If provided, execute as a background task and return
CreateTaskResult. If None (default), execute synchronously and
return ResourceResult.
Returns:
ResourceResult with contents.
May return CreateTaskResult if called in MCP context with task metadata.
ResourceResult when task_meta is None.
CreateTaskResult when task_meta is provided.
Raises:
NotFoundError: If resource not found or disabled
ResourceError: If resource read fails
"""
# Note: fn_key enrichment happens in each component's _read() method,
# not here, because resources and templates use different key formats:
# - Resources use Resource.make_key(uri) for the concrete URI
# - Templates use self.key which is the template pattern
async with fastmcp.server.context.Context(fastmcp=self) as ctx:
if run_middleware:
uri_param = AnyUrl(uri)
@ -1255,16 +1281,14 @@ class FastMCP(Generic[LifespanResultT]):
method="resources/read",
fastmcp_context=ctx,
)
result = await self._run_middleware(
return await self._run_middleware(
context=mw_context,
call_next=lambda context: self.read_resource(
str(context.message.uri),
run_middleware=False,
task_meta=task_meta,
),
)
if isinstance(result, mcp.types.CreateTaskResult):
return result
return result
# Core logic: find and read resource
# First pass: try concrete resources from all providers
@ -1272,10 +1296,7 @@ class FastMCP(Generic[LifespanResultT]):
resource = await provider.get_resource(uri)
if resource is not None and self._is_component_enabled(resource):
try:
result = await resource._read()
if isinstance(result, mcp.types.CreateTaskResult):
return result
return result
return await resource._read(task_meta=task_meta)
except (FastMCPError, McpError):
logger.exception(f"Error reading resource {uri!r}")
raise
@ -1296,10 +1317,9 @@ class FastMCP(Generic[LifespanResultT]):
params = template.matches(uri)
if params is not None:
try:
result = await template._read(uri, params)
if isinstance(result, mcp.types.CreateTaskResult):
return result
return result
return await template._read(
uri, params, task_meta=task_meta
)
except (FastMCPError, McpError):
logger.exception(f"Error reading resource {uri!r}")
raise
@ -1536,17 +1556,18 @@ class FastMCP(Generic[LifespanResultT]):
)
try:
# Extract SEP-1686 task metadata from request context
# Extract SEP-1686 task metadata from request context.
# NOTE: fn_key is NOT set here. The component (or provider wrapper) will
# enrich fn_key with self.key. For mounted servers, the provider wrapper
# sets fn_key to the parent's namespaced key, ensuring Docket finds the
# correctly registered function.
task_meta: TaskMeta | None = None
try:
ctx = self._mcp_server.request_context
if ctx.experimental.is_task:
mcp_task_meta = ctx.experimental.task_metadata
task_meta_dict = mcp_task_meta.model_dump(exclude_none=True)
task_meta = TaskMeta(
ttl=task_meta_dict.get("ttl"),
fn_key=Tool.make_key(key),
)
task_meta = TaskMeta(ttl=task_meta_dict.get("ttl"))
except (AttributeError, LookupError):
pass
@ -1566,8 +1587,9 @@ class FastMCP(Generic[LifespanResultT]):
) -> mcp.types.ReadResourceResult | mcp.types.CreateTaskResult:
"""Handle MCP 'readResource' requests.
Sets task metadata contextvar and calls read_resource(). The resource's
_read() method handles the backgrounding decision.
Extracts task metadata from MCP request context and passes it explicitly
to read_resource(). The resource's _read() method handles the backgrounding
decision, ensuring middleware runs before Docket.
Args:
uri: The resource URI
@ -1575,33 +1597,31 @@ class FastMCP(Generic[LifespanResultT]):
Returns:
ReadResourceResult or CreateTaskResult for background execution
"""
from fastmcp.server.dependencies import _docket_fn_key, _task_metadata
logger.debug(f"[{self.name}] Handler called: read_resource %s", uri)
try:
# Extract SEP-1686 task metadata from request context
task_meta_dict: dict[str, Any] | None = None
# Extract SEP-1686 task metadata from request context.
# NOTE: fn_key is NOT set here for resources because we don't know yet
# if the URI will be handled by a direct resource or a template. Templates
# need their pattern-based key for Docket lookup, not the concrete URI.
# The component (or provider wrapper) will enrich fn_key with self.key.
# For mounted servers, the provider wrapper sets fn_key to the parent's
# namespaced key, ensuring Docket finds the correctly registered function.
task_meta: TaskMeta | None = None
try:
ctx = self._mcp_server.request_context
if ctx.experimental.is_task:
task_meta = ctx.experimental.task_metadata
task_meta_dict = task_meta.model_dump(exclude_none=True)
mcp_task_meta = ctx.experimental.task_metadata
task_meta_dict = mcp_task_meta.model_dump(exclude_none=True)
task_meta = TaskMeta(ttl=task_meta_dict.get("ttl"))
except (AttributeError, LookupError):
pass
# Set contextvars so resource._read() can access them
task_token = _task_metadata.set(task_meta_dict)
key_token = _docket_fn_key.set(Resource.make_key(str(uri)))
try:
result = await self.read_resource(str(uri))
result = await self.read_resource(str(uri), task_meta=task_meta)
if isinstance(result, mcp.types.CreateTaskResult):
return result
return result.to_mcp_result(uri)
finally:
_task_metadata.reset(task_token)
_docket_fn_key.reset(key_token)
if isinstance(result, mcp.types.CreateTaskResult):
return result
return result.to_mcp_result(uri)
except DisabledError as e:
raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e
except NotFoundError:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import inspect
import warnings
from collections.abc import Callable
from dataclasses import dataclass
from dataclasses import dataclass, replace
from typing import (
TYPE_CHECKING,
Annotated,
@ -12,6 +12,7 @@ from typing import (
Generic,
TypeAlias,
get_type_hints,
overload,
)
import mcp.types
@ -277,6 +278,20 @@ class Tool(FastMCPComponent):
structured_content={"result": structured} if wrap_result else structured,
)
@overload
async def _run(
self,
arguments: dict[str, Any],
task_meta: None = None,
) -> ToolResult: ...
@overload
async def _run(
self,
arguments: dict[str, Any],
task_meta: TaskMeta,
) -> mcp.types.CreateTaskResult: ...
async def _run(
self,
arguments: dict[str, Any],
@ -290,8 +305,13 @@ class Tool(FastMCPComponent):
Args:
arguments: Tool arguments
task_meta: If provided, execute as background task. If None, execute
synchronously.
task_meta: If provided, execute as background task and return
CreateTaskResult. If None (default), execute synchronously and
return ToolResult.
Returns:
ToolResult when task_meta is None.
CreateTaskResult when task_meta is provided.
Subclasses can override this to customize task routing behavior.
For example, FastMCPProviderTool overrides to delegate to child
@ -299,6 +319,10 @@ class Tool(FastMCPComponent):
"""
from fastmcp.server.tasks.routing import check_background_task
# Enrich task_meta with fn_key if not already set (fallback for programmatic API)
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=self.key)
task_result = await check_background_task(
component=self,
task_type="tool",

View file

@ -1,7 +1,7 @@
import pytest
from pydantic import AnyUrl, BaseModel
from fastmcp.resources.resource import FunctionResource, ResourceContent, ResourceResult
from fastmcp.resources.resource import FunctionResource, ResourceContent
class TestFunctionResource:
@ -42,7 +42,6 @@ class TestFunctionResource:
# _read() converts to ResourceResult
result = await resource._read()
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
assert result.contents[0].content == "Hello, world!"
assert result.contents[0].mime_type == "text/plain"
@ -64,7 +63,6 @@ class TestFunctionResource:
# _read() converts to ResourceResult
result = await resource._read()
assert isinstance(result, ResourceResult)
assert result.contents[0].content == b"Hello, world!"
async def test_dict_return_raises_type_error(self):
@ -159,7 +157,6 @@ class TestFunctionResource:
# _read() converts to ResourceResult
result = await resource._read()
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Hello, world!"
assert result.contents[0].mime_type == "text/plain"

View file

@ -6,7 +6,7 @@ from pydantic import BaseModel
from fastmcp import Context, FastMCP
from fastmcp.resources import ResourceTemplate
from fastmcp.resources.resource import FunctionResource, ResourceResult
from fastmcp.resources.resource import FunctionResource
from fastmcp.resources.template import match_uri_template
@ -188,7 +188,6 @@ class TestResourceTemplate:
# _read() wraps in ResourceResult
resource_result = await resource._read()
assert isinstance(resource_result, ResourceResult)
assert len(resource_result.contents) == 1
assert resource_result.contents[0].content == "key=foo, value=123"

View file

@ -303,7 +303,6 @@ class TestResourceConvertResult:
resource = FunctionResource(uri=AnyUrl("test://test"), name="test", fn=fn)
result = await resource._read()
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
assert result.contents[0].content == "hello world"

View file

@ -7,7 +7,6 @@ import pytest
from fastmcp import Client, FastMCP
from fastmcp.exceptions import ToolError
from fastmcp.resources.resource import ResourceResult
from fastmcp.server.context import Context
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
from fastmcp.tools.tool import ToolResult
@ -514,7 +513,6 @@ class TestApplyMiddlewareParameter:
result = await server.read_resource("resource://test")
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
assert result.contents[0].content == "test content"
assert recording.assert_called(hook="on_read_resource", times=1)
@ -532,7 +530,6 @@ class TestApplyMiddlewareParameter:
result = await server.read_resource("resource://test", run_middleware=False)
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
assert result.contents[0].content == "test content"
# Middleware should not have been called
@ -551,7 +548,6 @@ class TestApplyMiddlewareParameter:
result = await server.read_resource("resource://items/42", run_middleware=False)
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
assert result.contents[0].content == "item 42"
assert len(recording.calls) == 0

View file

@ -82,7 +82,6 @@ class TestResourceTemplates:
return f"Data for {name}"
result = await mcp.read_resource("resource://test/data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Data for test"
async def test_resource_mismatched_params(self):
@ -107,7 +106,6 @@ class TestResourceTemplates:
return f"Data for {org}/{repo}"
result = await mcp.read_resource("resource://cursor/fastmcp/data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Data for cursor/fastmcp"
async def test_resource_multiple_mismatched_params(self):
@ -132,7 +130,6 @@ class TestResourceTemplates:
return str(sum(int(v) for v in kwargs.values()))
result = await mcp.read_resource("test://1/2/3")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "6"
async def test_template_with_default_params(self):
@ -148,11 +145,9 @@ class TestResourceTemplates:
assert templates[0].uri_template == "math://add/{x}"
result = await mcp.read_resource("math://add/5")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "15"
result2 = await mcp.read_resource("math://add/7")
assert isinstance(result2, ResourceResult)
assert result2.contents[0].content == "17"
async def test_template_to_resource_conversion(self):
@ -168,7 +163,6 @@ class TestResourceTemplates:
assert templates[0].uri_template == "resource://{name}/data"
result = await mcp.read_resource("resource://test/data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Data for test"
async def test_template_decorator_with_tags(self):
@ -190,7 +184,6 @@ class TestResourceTemplates:
return f"Template resource: {param}"
result = await mcp.read_resource("resource://test/data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Template resource: test/data"
async def test_template_with_query_params(self):
@ -202,15 +195,12 @@ class TestResourceTemplates:
return f"id={id}, format={format}, limit={limit}"
result = await mcp.read_resource("data://123")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "id=123, format=json, limit=10"
result = await mcp.read_resource("data://123?format=xml")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "id=123, format=xml, limit=10"
result = await mcp.read_resource("data://123?format=csv&limit=50")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "id=123, format=csv, limit=50"
async def test_templates_match_in_order_of_definition(self):
@ -226,11 +216,9 @@ class TestResourceTemplates:
return f"Template resource 2: {x}/{y}"
result = await mcp.read_resource("resource://a/b/c")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Template resource 1: a/b/c"
result = await mcp.read_resource("resource://a/b")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Template resource 1: a/b"
async def test_templates_shadow_each_other_reorder(self):
@ -246,11 +234,9 @@ class TestResourceTemplates:
return f"Template resource 2: {param}"
result = await mcp.read_resource("resource://a/b/c")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Template resource 2: a/b/c"
result = await mcp.read_resource("resource://a/b")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Template resource 1: a/b"
async def test_resource_template_with_annotations(self):
@ -324,7 +310,6 @@ class TestResourceDecorator:
return "Hello, world!"
result = await mcp.read_resource("resource://data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Hello, world!"
async def test_resource_decorator_incorrect_usage(self):
@ -350,7 +335,6 @@ class TestResourceDecorator:
assert resources[0].name == "custom-data"
result = await mcp.read_resource("resource://data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Hello, world!"
async def test_resource_decorator_with_description(self):
@ -395,7 +379,6 @@ class TestResourceDecorator:
)
result = await mcp.read_resource("resource://data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "My prefix: Hello, world!"
async def test_resource_decorator_classmethod(self):
@ -415,7 +398,6 @@ class TestResourceDecorator:
)
result = await mcp.read_resource("resource://data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Class prefix: Hello, world!"
async def test_resource_decorator_classmethod_error(self):
@ -439,7 +421,6 @@ class TestResourceDecorator:
return "Static Hello, world!"
result = await mcp.read_resource("resource://data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Static Hello, world!"
async def test_resource_decorator_async_function(self):
@ -450,7 +431,6 @@ class TestResourceDecorator:
return "Async Hello, world!"
result = await mcp.read_resource("resource://data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Async Hello, world!"
async def test_resource_decorator_staticmethod_order(self):
@ -464,7 +444,6 @@ class TestResourceDecorator:
return "Static Hello, world!"
result = await mcp.read_resource("resource://data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Static Hello, world!"
async def test_resource_decorator_with_meta(self):
@ -499,7 +478,6 @@ class TestResourceDecorator:
)
result = await mcp.read_resource("resource://widget")
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
assert result.contents[0].content == "<widget>content</widget>"
assert result.contents[0].mime_type == "text/html"
@ -521,7 +499,6 @@ class TestResourceDecorator:
)
result = await mcp.read_resource("resource://binary")
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
assert result.contents[0].content == b"\x00\x01\x02"
assert result.contents[0].meta == {"encoding": "raw"}
@ -535,7 +512,6 @@ class TestResourceDecorator:
return ResourceResult([ResourceContent(content="plain content")])
result = await mcp.read_resource("resource://plain")
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
assert result.contents[0].content == "plain content"
assert result.contents[0].meta is None
@ -555,7 +531,6 @@ class TestTemplateDecorator:
assert templates[0].uri_template == "resource://{name}/data"
result = await mcp.read_resource("resource://test/data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Data for test"
async def test_template_decorator_incorrect_usage(self):
@ -581,7 +556,6 @@ class TestTemplateDecorator:
assert templates[0].name == "custom-template"
result = await mcp.read_resource("resource://test/data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Data for test"
async def test_template_decorator_with_description(self):
@ -614,7 +588,6 @@ class TestTemplateDecorator:
mcp.add_template(template)
result = await mcp.read_resource("resource://test/data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "My prefix: Data for test"
async def test_template_decorator_classmethod(self):
@ -635,7 +608,6 @@ class TestTemplateDecorator:
mcp.add_template(template)
result = await mcp.read_resource("resource://test/data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Class prefix: Data for test"
async def test_template_decorator_staticmethod(self):
@ -648,7 +620,6 @@ class TestTemplateDecorator:
return f"Static Data for {name}"
result = await mcp.read_resource("resource://test/data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Static Data for test"
async def test_template_decorator_async_function(self):
@ -659,7 +630,6 @@ class TestTemplateDecorator:
return f"Async Data for {name}"
result = await mcp.read_resource("resource://test/data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Async Data for test"
async def test_template_decorator_with_tags(self):

View file

@ -0,0 +1,291 @@
"""
Tests for the explicit task_meta parameter on FastMCP.read_resource().
These tests verify that the task_meta parameter provides explicit control
over sync vs task execution for resources and resource templates.
"""
import pytest
from mcp.shared.exceptions import McpError
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.resources.resource import Resource, ResourceResult
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.tasks.config import TaskMeta
class TestResourceTaskMetaParameter:
"""Tests for task_meta parameter on FastMCP.read_resource()."""
async def test_task_meta_none_returns_resource_result(self):
"""With task_meta=None (default), read_resource returns ResourceResult."""
server = FastMCP("test")
@server.resource("data://test")
async def simple_resource() -> str:
return "hello world"
result = await server.read_resource("data://test")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "hello world"
async def test_task_meta_none_on_task_enabled_resource_still_returns_result(self):
"""Even for task=True resources, task_meta=None returns ResourceResult."""
server = FastMCP("test")
@server.resource("data://test", task=True)
async def task_enabled_resource() -> str:
return "hello world"
# Without task_meta, should execute synchronously
result = await server.read_resource("data://test")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "hello world"
async def test_task_meta_on_forbidden_resource_raises_error(self):
"""Providing task_meta to a task=False resource raises McpError."""
server = FastMCP("test")
@server.resource("data://test", task=False)
async def sync_only_resource() -> str:
return "hello"
with pytest.raises(McpError) as exc_info:
await server.read_resource("data://test", task_meta=TaskMeta())
assert "does not support task-augmented execution" in str(exc_info.value)
async def test_task_meta_fn_key_enrichment_for_resource(self):
"""Verify that fn_key enrichment uses Resource.make_key()."""
resource_uri = "data://my-resource"
expected_key = Resource.make_key(resource_uri)
assert expected_key == "resource:data://my-resource"
async def test_task_meta_fn_key_enrichment_for_template(self):
"""Verify that fn_key enrichment uses ResourceTemplate.make_key()."""
template_pattern = "data://{id}"
expected_key = ResourceTemplate.make_key(template_pattern)
assert expected_key == "template:data://{id}"
class TestResourceTemplateTaslMeta:
"""Tests for task_meta with resource templates."""
async def test_template_task_meta_none_returns_resource_result(self):
"""With task_meta=None, template read returns ResourceResult."""
server = FastMCP("test")
@server.resource("item://{id}")
async def get_item(id: str) -> str:
return f"Item {id}"
result = await server.read_resource("item://42")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Item 42"
async def test_template_task_meta_on_task_enabled_template_returns_result(self):
"""Even for task=True templates, task_meta=None returns ResourceResult."""
server = FastMCP("test")
@server.resource("item://{id}", task=True)
async def get_item(id: str) -> str:
return f"Item {id}"
# Without task_meta, should execute synchronously
result = await server.read_resource("item://42")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "Item 42"
async def test_template_task_meta_on_forbidden_template_raises_error(self):
"""Providing task_meta to a task=False template raises McpError."""
server = FastMCP("test")
@server.resource("item://{id}", task=False)
async def sync_only_template(id: str) -> str:
return f"Item {id}"
with pytest.raises(McpError) as exc_info:
await server.read_resource("item://42", task_meta=TaskMeta())
assert "does not support task-augmented execution" in str(exc_info.value)
class TestResourceTaskMetaClientIntegration:
"""Tests that task_meta works correctly with the Client for resources."""
async def test_client_read_resource_without_task_gets_immediate_result(self):
"""Client without task=True gets immediate result."""
server = FastMCP("test")
@server.resource("data://test", task=True)
async def immediate_resource() -> str:
return "hello"
async with Client(server) as client:
result = await client.read_resource("data://test")
# Should get ReadResourceResult directly
assert "hello" in str(result)
async def test_client_read_resource_with_task_creates_task(self):
"""Client with task=True creates a background task."""
server = FastMCP("test")
@server.resource("data://test", task=True)
async def task_resource() -> str:
return "hello"
async with Client(server) as client:
from fastmcp.client.tasks import ResourceTask
task = await client.read_resource("data://test", task=True)
assert isinstance(task, ResourceTask)
# Wait for result
result = await task.result()
assert "hello" in str(result)
async def test_client_read_template_with_task_creates_task(self):
"""Client with task=True on template creates a background task."""
server = FastMCP("test")
@server.resource("item://{id}", task=True)
async def get_item(id: str) -> str:
return f"Item {id}"
async with Client(server) as client:
from fastmcp.client.tasks import ResourceTask
task = await client.read_resource("item://42", task=True)
assert isinstance(task, ResourceTask)
# Wait for result
result = await task.result()
assert "Item 42" in str(result)
class TestResourceTaskMetaDirectServerCall:
"""Tests for direct server read_resource calls with task_meta."""
async def test_resource_can_read_another_resource_with_task(self):
"""A resource can read another resource as a background task."""
server = FastMCP("test")
@server.resource("data://inner", task=True)
async def inner_resource() -> str:
return "inner data"
@server.tool
async def outer_tool() -> str:
# Read inner resource as background task
result = await server.read_resource("data://inner", task_meta=TaskMeta())
# Should get CreateTaskResult since we provided task_meta
return f"Created task: {result.task.taskId}"
async with Client(server) as client:
result = await client.call_tool("outer_tool", {})
assert "Created task:" in str(result)
async def test_resource_can_read_another_resource_synchronously(self):
"""A resource can read another resource synchronously (no task_meta)."""
server = FastMCP("test")
@server.resource("data://inner", task=True)
async def inner_resource() -> str:
return "inner data"
@server.tool
async def outer_tool() -> str:
# Read inner resource synchronously (no task_meta)
result = await server.read_resource("data://inner")
# Should get ResourceResult directly
return f"Got result: {result.contents[0].content}"
async with Client(server) as client:
result = await client.call_tool("outer_tool", {})
assert "Got result: inner data" in str(result)
async def test_resource_can_read_template_with_task(self):
"""A tool can read a resource template as a background task."""
server = FastMCP("test")
@server.resource("item://{id}", task=True)
async def get_item(id: str) -> str:
return f"Item {id}"
@server.tool
async def outer_tool() -> str:
result = await server.read_resource("item://99", task_meta=TaskMeta())
return f"Created task: {result.task.taskId}"
async with Client(server) as client:
result = await client.call_tool("outer_tool", {})
assert "Created task:" in str(result)
async def test_resource_can_read_with_custom_ttl(self):
"""A tool can read a resource as a background task with custom TTL."""
server = FastMCP("test")
@server.resource("data://inner", task=True)
async def inner_resource() -> str:
return "inner data"
@server.tool
async def outer_tool() -> str:
custom_ttl = 45000 # 45 seconds
result = await server.read_resource(
"data://inner", task_meta=TaskMeta(ttl=custom_ttl)
)
return f"Task TTL: {result.task.ttl}"
async with Client(server) as client:
result = await client.call_tool("outer_tool", {})
assert "Task TTL: 45000" in str(result)
class TestResourceTaskMetaTypeNarrowing:
"""Tests for type narrowing based on task_meta parameter."""
async def test_read_resource_without_task_meta_type_is_resource_result(self):
"""Calling read_resource without task_meta returns ResourceResult type."""
server = FastMCP("test")
@server.resource("data://test")
async def simple_resource() -> str:
return "hello"
# This should type-check as ResourceResult, not the union type
result = await server.read_resource("data://test")
# No isinstance check needed - type is narrowed by overload
content = result.contents[0].content
assert content == "hello"
async def test_read_resource_with_task_meta_type_is_create_task_result(self):
"""Calling read_resource with task_meta returns CreateTaskResult type."""
server = FastMCP("test")
@server.resource("data://test", task=True)
async def task_resource() -> str:
return "hello"
async with Client(server) as client:
# Need to use client to get full task infrastructure
from fastmcp.client.tasks import ResourceTask
task = await client.read_resource("data://test", task=True)
assert isinstance(task, ResourceTask)
# For direct server call, we need the Client context for Docket
# This test verifies the overload works via client integration
result = await task.result()
assert "hello" in str(result)

View file

@ -773,3 +773,147 @@ class TestMiddlewareWithMountedTasks:
"parent:after",
"grandchild:template",
]
class TestMountedTasksWithTaskMetaParameter:
"""Test mounted components called directly with task_meta parameter.
These tests verify the programmatic API where server.call_tool() or
server.read_resource() is called with an explicit task_meta parameter,
as opposed to using the Client with task=True.
Direct server calls require a running server context, so we use an outer
tool that makes the direct call internally.
"""
async def test_mounted_tool_with_task_meta_creates_task(self):
"""Mounted tool called with task_meta returns CreateTaskResult."""
from fastmcp.server.tasks.config import TaskMeta
child = FastMCP("Child")
@child.tool(task=True)
async def add(a: int, b: int) -> int:
return a + b
parent = FastMCP("Parent")
parent.mount(child, namespace="child")
@parent.tool
async def outer() -> str:
# Direct call with task_meta from within server context
result = await parent.call_tool(
"child_add", {"a": 2, "b": 3}, task_meta=TaskMeta(ttl=300)
)
assert isinstance(result, mt.CreateTaskResult)
return f"task:{result.task.taskId}"
async with Client(parent) as client:
result = await client.call_tool("outer", {})
assert "task:" in str(result)
async def test_mounted_resource_with_task_meta_creates_task(self):
"""Mounted resource called with task_meta returns CreateTaskResult."""
from fastmcp.server.tasks.config import TaskMeta
child = FastMCP("Child")
@child.resource("data://info", task=True)
async def get_info() -> str:
return "child info"
parent = FastMCP("Parent")
parent.mount(child, namespace="child")
@parent.tool
async def outer() -> str:
result = await parent.read_resource(
"data://child/info", task_meta=TaskMeta(ttl=300)
)
assert isinstance(result, mt.CreateTaskResult)
return f"task:{result.task.taskId}"
async with Client(parent) as client:
result = await client.call_tool("outer", {})
assert "task:" in str(result)
async def test_mounted_template_with_task_meta_creates_task(self):
"""Mounted resource template with task_meta returns CreateTaskResult."""
from fastmcp.server.tasks.config import TaskMeta
child = FastMCP("Child")
@child.resource("item://{id}", task=True)
async def get_item(id: str) -> str:
return f"item-{id}"
parent = FastMCP("Parent")
parent.mount(child, namespace="child")
@parent.tool
async def outer() -> str:
result = await parent.read_resource(
"item://child/42", task_meta=TaskMeta(ttl=300)
)
assert isinstance(result, mt.CreateTaskResult)
return f"task:{result.task.taskId}"
async with Client(parent) as client:
result = await client.call_tool("outer", {})
assert "task:" in str(result)
async def test_deeply_nested_tool_with_task_meta(self):
"""Three-level nested tool works with task_meta."""
from fastmcp.server.tasks.config import TaskMeta
grandchild = FastMCP("Grandchild")
@grandchild.tool(task=True)
async def compute(n: int) -> int:
return n * 3
child = FastMCP("Child")
child.mount(grandchild, namespace="gc")
parent = FastMCP("Parent")
parent.mount(child, namespace="c")
@parent.tool
async def outer() -> str:
result = await parent.call_tool(
"c_gc_compute", {"n": 7}, task_meta=TaskMeta(ttl=300)
)
assert isinstance(result, mt.CreateTaskResult)
return f"task:{result.task.taskId}"
async with Client(parent) as client:
result = await client.call_tool("outer", {})
assert "task:" in str(result)
async def test_deeply_nested_template_with_task_meta(self):
"""Three-level nested template works with task_meta."""
from fastmcp.server.tasks.config import TaskMeta
grandchild = FastMCP("Grandchild")
@grandchild.resource("doc://{name}", task=True)
async def get_doc(name: str) -> str:
return f"doc: {name}"
child = FastMCP("Child")
child.mount(grandchild, namespace="gc")
parent = FastMCP("Parent")
parent.mount(child, namespace="c")
@parent.tool
async def outer() -> str:
result = await parent.read_resource(
"doc://c/gc/readme", task_meta=TaskMeta(ttl=300)
)
assert isinstance(result, mt.CreateTaskResult)
return f"task:{result.task.taskId}"
async with Client(parent) as client:
result = await client.call_tool("outer", {})
assert "task:" in str(result)

View file

@ -9,7 +9,6 @@ from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.dependencies import CurrentContext, Depends
from fastmcp.prompts import PromptResult
from fastmcp.resources import ResourceResult
from fastmcp.server.context import Context
HUZZAH = "huzzah!"
@ -266,7 +265,6 @@ async def test_resource_with_dependency(mcp: FastMCP):
return f"Settings loaded from {storage}"
result = await mcp.read_resource("config://settings")
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
assert result.contents[0].content == "Settings loaded from /data/config"
@ -343,7 +341,6 @@ async def test_resource_template_with_dependency(mcp: FastMCP):
return f"Reading {base_path}/{filename}"
result = await mcp.read_resource("data://config.txt")
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
assert result.contents[0].content == "Reading /var/data/config.txt"
@ -399,7 +396,6 @@ async def test_async_resource_context_manager_stays_open(mcp: FastMCP):
return f"open={connection.is_open}"
result = await mcp.read_resource("data://config")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "open=True"
@ -415,7 +411,6 @@ async def test_async_resource_template_context_manager_stays_open(mcp: FastMCP):
return f"open={connection.is_open},user={user_id}"
result = await mcp.read_resource("user://123")
assert isinstance(result, ResourceResult)
assert isinstance(result.contents[0].content, str)
assert "open=True" in result.contents[0].content
@ -525,7 +520,6 @@ async def test_sync_resource_context_manager_stays_open(mcp: FastMCP):
return f"open={connection.is_open}"
result = await mcp.read_resource("data://sync")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "open=True"
assert not conn.is_open
@ -551,7 +545,6 @@ async def test_sync_resource_template_context_manager_stays_open(mcp: FastMCP):
return f"open={connection.is_open},item={item_id}"
result = await mcp.read_resource("item://456")
assert isinstance(result, ResourceResult)
assert isinstance(result.contents[0].content, str)
assert "open=True" in result.contents[0].content
assert not conn.is_open
@ -663,7 +656,6 @@ async def test_resource_dependency_cannot_be_overridden_externally(mcp: FastMCP)
# Normal call
result = await mcp.read_resource("data://config")
assert isinstance(result, ResourceResult)
assert isinstance(result.contents[0].content, str)
assert "API Key: real_api_key" in result.contents[0].content
@ -689,7 +681,6 @@ async def test_resource_template_dependency_cannot_be_overridden_externally(
# Normal call
result = await mcp.read_resource("user://123")
assert isinstance(result, ResourceResult)
assert isinstance(result.contents[0].content, str)
assert "User: 123, Token: real_token" in result.contents[0].content

View file

@ -10,7 +10,6 @@ from fastmcp.client import Client
from fastmcp.client.transports import FastMCPTransport, SSETransport
from fastmcp.exceptions import NotFoundError
from fastmcp.prompts import PromptResult
from fastmcp.resources import ResourceResult
from fastmcp.server.providers import FastMCPProvider, TransformingProvider
from fastmcp.server.providers.proxy import FastMCPProxy
from fastmcp.tools.tool import Tool
@ -143,7 +142,6 @@ class TestBasicMount:
# Test actual functionality
resource_result = await main_app.read_resource("data://config")
assert isinstance(resource_result, ResourceResult)
assert resource_result.contents[0].content == "Sub resource data"
async def test_mount_resource_templates_no_prefix(self):
@ -164,7 +162,6 @@ class TestBasicMount:
# Test actual functionality
template_result = await main_app.read_resource("users://123/info")
assert isinstance(template_result, ResourceResult)
assert template_result.contents[0].content == "Sub template for user 123"
async def test_mount_prompts_no_prefix(self):
@ -409,7 +406,6 @@ class TestPrefixConflictResolution:
# Test that reading the resource uses the first server's implementation
result = await main_app.read_resource("shared://data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "First app data"
async def test_first_server_wins_resources_same_prefix(self):
@ -438,7 +434,6 @@ class TestPrefixConflictResolution:
# Test that reading the resource uses the first server's implementation
result = await main_app.read_resource("shared://api/data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "First app data"
async def test_first_server_wins_resource_templates_no_prefix(self):
@ -469,7 +464,6 @@ class TestPrefixConflictResolution:
# Test that reading the resource uses the first server's implementation
result = await main_app.read_resource("users://123/profile")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "First app user 123"
async def test_first_server_wins_resource_templates_same_prefix(self):
@ -500,7 +494,6 @@ class TestPrefixConflictResolution:
# Test that reading the resource uses the first server's implementation
result = await main_app.read_resource("users://api/123/profile")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "First app user 123"
async def test_first_server_wins_prompts_no_prefix(self):
@ -639,7 +632,6 @@ class TestResourcesAndTemplates:
# Check that resource can be accessed
result = await main_app.read_resource("data://data/users")
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
# Note: The function returns "user1, user2" which is not valid JSON
# This test should be updated to return proper JSON or check the string directly
@ -663,7 +655,6 @@ class TestResourcesAndTemplates:
# Check template instantiation
result = await main_app.read_resource("users://api/123/profile")
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
profile = json.loads(result.contents[0].content)
assert profile["id"] == "123"
@ -688,7 +679,6 @@ class TestResourcesAndTemplates:
# Check access to the resource
result = await main_app.read_resource("data://data/config")
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
config = json.loads(result.contents[0].content)
assert config["version"] == "1.0"
@ -813,7 +803,6 @@ class TestProxyServer:
# Resource should be accessible through main app
result = await main_app.read_resource("config://proxy/settings")
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
config = json.loads(result.contents[0].content)
assert config["api_key"] == "12345"
@ -1264,12 +1253,10 @@ class TestDeeplyNestedMount:
# Resource at level 2 should work
result = await root.read_resource("middle://middle/data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "middle data"
# Resource at level 3 should also work
result = await root.read_resource("leaf://middle/leaf/data")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "leaf data"
async def test_three_level_nested_resource_template_invocation(self):
@ -1291,12 +1278,10 @@ class TestDeeplyNestedMount:
# Resource template at level 2 should work
result = await root.read_resource("middle://middle/item/42")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "middle item 42"
# Resource template at level 3 should also work
result = await root.read_resource("leaf://middle/leaf/item/99")
assert isinstance(result, ResourceResult)
assert result.contents[0].content == "leaf item 99"
async def test_three_level_nested_prompt_invocation(self):

View file

@ -8,7 +8,6 @@ from mcp.types import AnyUrl, TextContent
from fastmcp import FastMCP
from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
from fastmcp.resources import ResourceResult
from fastmcp.resources.resource import FunctionResource, Resource
from fastmcp.resources.template import FunctionResourceTemplate, ResourceTemplate
from fastmcp.server.providers import Provider
@ -389,7 +388,6 @@ class TestProviderExecutionMethods:
result = await mcp.read_resource("test://data")
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
assert result.contents[0].content == "hello world"
@ -412,7 +410,6 @@ class TestProviderExecutionMethods:
result = await mcp.read_resource("data://files/test.txt")
assert isinstance(result, ResourceResult)
assert len(result.contents) == 1
assert result.contents[0].content == "content of test.txt"