mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Merge pull request #2645 from jlowin/2642-task-custom-name
Fix task execution for tools with custom names
This commit is contained in:
commit
8d758baeda
4 changed files with 95 additions and 38 deletions
|
|
@ -10,7 +10,7 @@ dependencies = [
|
|||
"mcp>=1.24.0",
|
||||
"openapi-pydantic>=0.5.1",
|
||||
"platformdirs>=4.0.0",
|
||||
"pydocket>=0.15.5",
|
||||
"pydocket>=0.16.0",
|
||||
"rich>=13.9.4",
|
||||
"cyclopts>=4.0.0",
|
||||
"authlib>=1.6.5",
|
||||
|
|
|
|||
|
|
@ -112,23 +112,6 @@ if TYPE_CHECKING:
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def _create_named_fn_wrapper(fn: Callable[..., Any], name: str) -> Callable[..., Any]:
|
||||
"""Create a wrapper function with a custom __name__ for Docket registration.
|
||||
|
||||
Docket uses fn.__name__ as the key for function registration and lookup.
|
||||
When mounting servers, we need unique names to avoid collisions between
|
||||
mounted servers that have identically-named functions.
|
||||
"""
|
||||
import functools
|
||||
|
||||
@functools.wraps(fn)
|
||||
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
return await fn(*args, **kwargs)
|
||||
|
||||
wrapper.__name__ = name
|
||||
return wrapper
|
||||
|
||||
|
||||
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
||||
Transport = Literal["stdio", "http", "sse", "streamable-http"]
|
||||
|
||||
|
|
@ -441,7 +424,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
isinstance(tool, FunctionTool)
|
||||
and tool.task_config.mode != "forbidden"
|
||||
):
|
||||
docket.register(tool.fn)
|
||||
docket.register(tool.fn, names=[tool.key])
|
||||
|
||||
for prompt in self._prompt_manager._prompts.values():
|
||||
if (
|
||||
|
|
@ -449,42 +432,40 @@ class FastMCP(Generic[LifespanResultT]):
|
|||
and prompt.task_config.mode != "forbidden"
|
||||
):
|
||||
# task execution requires async fn (validated at creation time)
|
||||
docket.register(cast(Callable[..., Awaitable[Any]], prompt.fn))
|
||||
docket.register(
|
||||
cast(Callable[..., Awaitable[Any]], prompt.fn),
|
||||
names=[prompt.key],
|
||||
)
|
||||
|
||||
for resource in self._resource_manager._resources.values():
|
||||
if (
|
||||
isinstance(resource, FunctionResource)
|
||||
and resource.task_config.mode != "forbidden"
|
||||
):
|
||||
docket.register(resource.fn)
|
||||
docket.register(resource.fn, names=[resource.name])
|
||||
|
||||
for template in self._resource_manager._templates.values():
|
||||
if (
|
||||
isinstance(template, FunctionResourceTemplate)
|
||||
and template.task_config.mode != "forbidden"
|
||||
):
|
||||
docket.register(template.fn)
|
||||
docket.register(template.fn, names=[template.name])
|
||||
|
||||
# Register provider components
|
||||
for provider in self._providers:
|
||||
try:
|
||||
tasks = await provider.get_tasks()
|
||||
for tool in tasks.tools:
|
||||
named_fn = _create_named_fn_wrapper(tool.fn, tool.key)
|
||||
docket.register(named_fn)
|
||||
docket.register(tool.fn, names=[tool.key])
|
||||
for resource in tasks.resources:
|
||||
named_fn = _create_named_fn_wrapper(
|
||||
resource.fn, resource.name
|
||||
)
|
||||
docket.register(named_fn)
|
||||
docket.register(resource.fn, names=[resource.name])
|
||||
for template in tasks.templates:
|
||||
named_fn = _create_named_fn_wrapper(
|
||||
template.fn, template.name
|
||||
)
|
||||
docket.register(named_fn)
|
||||
docket.register(template.fn, names=[template.name])
|
||||
for prompt in tasks.prompts:
|
||||
named_fn = _create_named_fn_wrapper(prompt.fn, prompt.key)
|
||||
docket.register(named_fn)
|
||||
docket.register(
|
||||
cast(Callable[..., Awaitable[Any]], prompt.fn),
|
||||
names=[prompt.key],
|
||||
)
|
||||
except Exception as e:
|
||||
provider_name = getattr(
|
||||
provider, "server", provider
|
||||
|
|
|
|||
|
|
@ -308,3 +308,79 @@ async def test_multiple_components_same_name_different_tasks():
|
|||
# Prompt inheriting False (mode="forbidden") raises McpError
|
||||
with pytest.raises(McpError):
|
||||
await client.get_prompt("shared_name_prompt", task=True)
|
||||
|
||||
|
||||
async def test_task_with_custom_tool_name():
|
||||
"""Tools with custom names work correctly as tasks (issue #2642).
|
||||
|
||||
When a tool is registered with a custom name different from the function
|
||||
name, task execution should use the custom name for Docket lookup.
|
||||
"""
|
||||
mcp = FastMCP("test", tasks=True)
|
||||
|
||||
async def my_function() -> str:
|
||||
return "result from custom-named tool"
|
||||
|
||||
mcp.tool(my_function, name="custom-tool-name")
|
||||
|
||||
async with Client(mcp) as client:
|
||||
# Verify the tool is registered with its custom name in Docket
|
||||
docket = mcp.docket
|
||||
assert docket is not None
|
||||
assert "custom-tool-name" in docket.tasks
|
||||
|
||||
# Call the tool as a task using its custom name
|
||||
task = await client.call_tool("custom-tool-name", task=True)
|
||||
assert not task.returned_immediately
|
||||
result = await task
|
||||
assert result.data == "result from custom-named tool"
|
||||
|
||||
|
||||
async def test_task_with_custom_resource_name():
|
||||
"""Resources with custom names work correctly as tasks.
|
||||
|
||||
When a resource is registered with a custom name different from the function
|
||||
name, task execution should use the custom name for Docket lookup.
|
||||
"""
|
||||
mcp = FastMCP("test", tasks=True)
|
||||
|
||||
@mcp.resource("test://resource", name="custom-resource-name")
|
||||
async def my_resource_func() -> str:
|
||||
return "result from custom-named resource"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
# Verify the resource is registered with its custom name in Docket
|
||||
docket = mcp.docket
|
||||
assert docket is not None
|
||||
assert "custom-resource-name" in docket.tasks
|
||||
|
||||
# Call the resource as a task
|
||||
task = await client.read_resource("test://resource", task=True)
|
||||
assert not task.returned_immediately
|
||||
result = await task.result()
|
||||
assert result[0].text == "result from custom-named resource"
|
||||
|
||||
|
||||
async def test_task_with_custom_template_name():
|
||||
"""Resource templates with custom names work correctly as tasks.
|
||||
|
||||
When a template is registered with a custom name different from the function
|
||||
name, task execution should use the custom name for Docket lookup.
|
||||
"""
|
||||
mcp = FastMCP("test", tasks=True)
|
||||
|
||||
@mcp.resource("test://{item_id}", name="custom-template-name")
|
||||
async def my_template_func(item_id: str) -> str:
|
||||
return f"result for {item_id}"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
# Verify the template is registered with its custom name in Docket
|
||||
docket = mcp.docket
|
||||
assert docket is not None
|
||||
assert "custom-template-name" in docket.tasks
|
||||
|
||||
# Call the template as a task
|
||||
task = await client.read_resource("test://123", task=True)
|
||||
assert not task.returned_immediately
|
||||
result = await task.result()
|
||||
assert result[0].text == "result for 123"
|
||||
|
|
|
|||
8
uv.lock
generated
8
uv.lock
generated
|
|
@ -752,7 +752,7 @@ requires-dist = [
|
|||
{ name = "platformdirs", specifier = ">=4.0.0" },
|
||||
{ name = "py-key-value-aio", extras = ["disk", "keyring", "memory"], specifier = ">=0.3.0,<0.4.0" },
|
||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.11.7" },
|
||||
{ name = "pydocket", specifier = ">=0.15.5" },
|
||||
{ name = "pydocket", specifier = ">=0.16.0" },
|
||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
||||
{ name = "rich", specifier = ">=13.9.4" },
|
||||
|
|
@ -1775,7 +1775,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "pydocket"
|
||||
version = "0.15.5"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cloudpickle" },
|
||||
|
|
@ -1792,9 +1792,9 @@ dependencies = [
|
|||
{ name = "typer" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/86/77/842e41be3cf3592b971cf42b24cae76e282294f474dc2dbf7cd6808d1b09/pydocket-0.15.5.tar.gz", hash = "sha256:b3af47702a293dd1da2e5e0f8f73f27fd3b3c95e36de72a2f71026d16908d5ba", size = 277245, upload-time = "2025-12-12T22:28:47.32Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/d3/ff57fb8ba8180c621b17270a272ee8821a64962887c42962c5a342ab82a9/pydocket-0.16.0.tar.gz", hash = "sha256:675e829a7ea6e978fdbbe0ace342f24c8ec7cf8ed5980694215418e59ff86005", size = 286316, upload-time = "2025-12-18T15:14:53.139Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/c0/fdbc6e04e3369b90c6bf6567bc62871cf59e88550b94529821500dc807c1/pydocket-0.15.5-py3-none-any.whl", hash = "sha256:ad0d86c9a1bea394e875bcf8c793be2d0a7ebd1891bfe99e2e9eaf99ef0cb42e", size = 58517, upload-time = "2025-12-12T22:28:45.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/74/dcf56132e03a24e2591c5f2391902c871e5f1248e0fc175b9f1792e4afaf/pydocket-0.16.0-py3-none-any.whl", hash = "sha256:3d7d0a805bcb68a3a03d96d3c6ba8e607584ed14b0fe3879333a8fff652dda92", size = 61064, upload-time = "2025-12-18T15:14:51.506Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue