Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
5ee8f66
1
Parent(s): c028391
Return objects from decorators
Browse files
src/fastmcp/server/server.py
CHANGED
|
@@ -14,7 +14,7 @@ from contextlib import (
|
|
| 14 |
)
|
| 15 |
from functools import partial
|
| 16 |
from pathlib import Path
|
| 17 |
-
from typing import TYPE_CHECKING, Any, Generic, Literal
|
| 18 |
|
| 19 |
import anyio
|
| 20 |
import httpx
|
|
@@ -45,6 +45,7 @@ import fastmcp.server
|
|
| 45 |
import fastmcp.settings
|
| 46 |
from fastmcp.exceptions import NotFoundError
|
| 47 |
from fastmcp.prompts import Prompt, PromptManager
|
|
|
|
| 48 |
from fastmcp.resources import Resource, ResourceManager
|
| 49 |
from fastmcp.resources.template import ResourceTemplate
|
| 50 |
from fastmcp.server.auth.auth import OAuthProvider
|
|
@@ -55,7 +56,7 @@ from fastmcp.server.http import (
|
|
| 55 |
create_streamable_http_app,
|
| 56 |
)
|
| 57 |
from fastmcp.tools import ToolManager
|
| 58 |
-
from fastmcp.tools.tool import Tool
|
| 59 |
from fastmcp.utilities.cache import TimedCache
|
| 60 |
from fastmcp.utilities.logging import get_logger
|
| 61 |
from fastmcp.utilities.mcp_config import MCPConfig
|
|
@@ -510,6 +511,30 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 510 |
self._tool_manager.remove_tool(name)
|
| 511 |
self._cache.clear()
|
| 512 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
def tool(
|
| 514 |
self,
|
| 515 |
name_or_fn: str | AnyFunction | None = None,
|
|
@@ -519,7 +544,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 519 |
tags: set[str] | None = None,
|
| 520 |
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
| 521 |
exclude_args: list[str] | None = None,
|
| 522 |
-
) -> Callable[[AnyFunction],
|
| 523 |
"""Decorator to register a tool.
|
| 524 |
|
| 525 |
Tools can optionally request a Context object by adding a parameter with the
|
|
@@ -571,7 +596,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 571 |
fn = name_or_fn
|
| 572 |
tool_name = name # Use keyword name if provided, otherwise None
|
| 573 |
|
| 574 |
-
# Register the tool immediately and return the
|
| 575 |
tool = Tool.from_function(
|
| 576 |
fn,
|
| 577 |
name=tool_name,
|
|
@@ -582,7 +607,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 582 |
serializer=self._tool_serializer,
|
| 583 |
)
|
| 584 |
self.add_tool(tool)
|
| 585 |
-
return
|
| 586 |
|
| 587 |
elif isinstance(name_or_fn, str):
|
| 588 |
# Case 3: @tool("custom_name") - name passed as first argument
|
|
@@ -674,7 +699,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 674 |
description: str | None = None,
|
| 675 |
mime_type: str | None = None,
|
| 676 |
tags: set[str] | None = None,
|
| 677 |
-
) -> Callable[[AnyFunction],
|
| 678 |
"""Decorator to register a function as a resource.
|
| 679 |
|
| 680 |
The function will be called when the resource is read to generate its content.
|
|
@@ -728,7 +753,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 728 |
"Did you forget to call it? Use @resource('uri') instead of @resource"
|
| 729 |
)
|
| 730 |
|
| 731 |
-
def decorator(fn: AnyFunction) ->
|
| 732 |
from fastmcp.server.context import Context
|
| 733 |
|
| 734 |
# Check if this should be a template
|
|
@@ -750,6 +775,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 750 |
tags=tags,
|
| 751 |
)
|
| 752 |
self.add_template(template)
|
|
|
|
| 753 |
elif not has_uri_params and not has_func_params:
|
| 754 |
resource = Resource.from_function(
|
| 755 |
fn=fn,
|
|
@@ -760,14 +786,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 760 |
tags=tags,
|
| 761 |
)
|
| 762 |
self.add_resource(resource)
|
|
|
|
| 763 |
else:
|
| 764 |
raise ValueError(
|
| 765 |
"Invalid resource or template definition due to a "
|
| 766 |
"mismatch between URI parameters and function parameters."
|
| 767 |
)
|
| 768 |
|
| 769 |
-
return fn
|
| 770 |
-
|
| 771 |
return decorator
|
| 772 |
|
| 773 |
def add_prompt(self, prompt: Prompt) -> None:
|
|
@@ -779,6 +804,26 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 779 |
self._prompt_manager.add_prompt(prompt)
|
| 780 |
self._cache.clear()
|
| 781 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
def prompt(
|
| 783 |
self,
|
| 784 |
name_or_fn: str | AnyFunction | None = None,
|
|
@@ -786,7 +831,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 786 |
name: str | None = None,
|
| 787 |
description: str | None = None,
|
| 788 |
tags: set[str] | None = None,
|
| 789 |
-
) -> Callable[[AnyFunction],
|
| 790 |
"""Decorator to register a prompt.
|
| 791 |
|
| 792 |
Prompts can optionally request a Context object by adding a parameter with the
|
|
@@ -867,7 +912,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 867 |
)
|
| 868 |
self.add_prompt(prompt)
|
| 869 |
|
| 870 |
-
return
|
| 871 |
|
| 872 |
elif isinstance(name_or_fn, str):
|
| 873 |
# Case 3: @prompt("custom_name") - name passed as first argument
|
|
|
|
| 14 |
)
|
| 15 |
from functools import partial
|
| 16 |
from pathlib import Path
|
| 17 |
+
from typing import TYPE_CHECKING, Any, Generic, Literal, overload
|
| 18 |
|
| 19 |
import anyio
|
| 20 |
import httpx
|
|
|
|
| 45 |
import fastmcp.settings
|
| 46 |
from fastmcp.exceptions import NotFoundError
|
| 47 |
from fastmcp.prompts import Prompt, PromptManager
|
| 48 |
+
from fastmcp.prompts.prompt import FunctionPrompt
|
| 49 |
from fastmcp.resources import Resource, ResourceManager
|
| 50 |
from fastmcp.resources.template import ResourceTemplate
|
| 51 |
from fastmcp.server.auth.auth import OAuthProvider
|
|
|
|
| 56 |
create_streamable_http_app,
|
| 57 |
)
|
| 58 |
from fastmcp.tools import ToolManager
|
| 59 |
+
from fastmcp.tools.tool import FunctionTool, Tool
|
| 60 |
from fastmcp.utilities.cache import TimedCache
|
| 61 |
from fastmcp.utilities.logging import get_logger
|
| 62 |
from fastmcp.utilities.mcp_config import MCPConfig
|
|
|
|
| 511 |
self._tool_manager.remove_tool(name)
|
| 512 |
self._cache.clear()
|
| 513 |
|
| 514 |
+
@overload
|
| 515 |
+
def tool(
|
| 516 |
+
self,
|
| 517 |
+
name_or_fn: AnyFunction,
|
| 518 |
+
*,
|
| 519 |
+
name: str | None = None,
|
| 520 |
+
description: str | None = None,
|
| 521 |
+
tags: set[str] | None = None,
|
| 522 |
+
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
| 523 |
+
exclude_args: list[str] | None = None,
|
| 524 |
+
) -> FunctionTool: ...
|
| 525 |
+
|
| 526 |
+
@overload
|
| 527 |
+
def tool(
|
| 528 |
+
self,
|
| 529 |
+
name_or_fn: str | None = None,
|
| 530 |
+
*,
|
| 531 |
+
name: str | None = None,
|
| 532 |
+
description: str | None = None,
|
| 533 |
+
tags: set[str] | None = None,
|
| 534 |
+
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
| 535 |
+
exclude_args: list[str] | None = None,
|
| 536 |
+
) -> Callable[[AnyFunction], FunctionTool]: ...
|
| 537 |
+
|
| 538 |
def tool(
|
| 539 |
self,
|
| 540 |
name_or_fn: str | AnyFunction | None = None,
|
|
|
|
| 544 |
tags: set[str] | None = None,
|
| 545 |
annotations: ToolAnnotations | dict[str, Any] | None = None,
|
| 546 |
exclude_args: list[str] | None = None,
|
| 547 |
+
) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
|
| 548 |
"""Decorator to register a tool.
|
| 549 |
|
| 550 |
Tools can optionally request a Context object by adding a parameter with the
|
|
|
|
| 596 |
fn = name_or_fn
|
| 597 |
tool_name = name # Use keyword name if provided, otherwise None
|
| 598 |
|
| 599 |
+
# Register the tool immediately and return the tool object
|
| 600 |
tool = Tool.from_function(
|
| 601 |
fn,
|
| 602 |
name=tool_name,
|
|
|
|
| 607 |
serializer=self._tool_serializer,
|
| 608 |
)
|
| 609 |
self.add_tool(tool)
|
| 610 |
+
return tool
|
| 611 |
|
| 612 |
elif isinstance(name_or_fn, str):
|
| 613 |
# Case 3: @tool("custom_name") - name passed as first argument
|
|
|
|
| 699 |
description: str | None = None,
|
| 700 |
mime_type: str | None = None,
|
| 701 |
tags: set[str] | None = None,
|
| 702 |
+
) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
|
| 703 |
"""Decorator to register a function as a resource.
|
| 704 |
|
| 705 |
The function will be called when the resource is read to generate its content.
|
|
|
|
| 753 |
"Did you forget to call it? Use @resource('uri') instead of @resource"
|
| 754 |
)
|
| 755 |
|
| 756 |
+
def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
|
| 757 |
from fastmcp.server.context import Context
|
| 758 |
|
| 759 |
# Check if this should be a template
|
|
|
|
| 775 |
tags=tags,
|
| 776 |
)
|
| 777 |
self.add_template(template)
|
| 778 |
+
return template
|
| 779 |
elif not has_uri_params and not has_func_params:
|
| 780 |
resource = Resource.from_function(
|
| 781 |
fn=fn,
|
|
|
|
| 786 |
tags=tags,
|
| 787 |
)
|
| 788 |
self.add_resource(resource)
|
| 789 |
+
return resource
|
| 790 |
else:
|
| 791 |
raise ValueError(
|
| 792 |
"Invalid resource or template definition due to a "
|
| 793 |
"mismatch between URI parameters and function parameters."
|
| 794 |
)
|
| 795 |
|
|
|
|
|
|
|
| 796 |
return decorator
|
| 797 |
|
| 798 |
def add_prompt(self, prompt: Prompt) -> None:
|
|
|
|
| 804 |
self._prompt_manager.add_prompt(prompt)
|
| 805 |
self._cache.clear()
|
| 806 |
|
| 807 |
+
@overload
|
| 808 |
+
def prompt(
|
| 809 |
+
self,
|
| 810 |
+
name_or_fn: AnyFunction,
|
| 811 |
+
*,
|
| 812 |
+
name: str | None = None,
|
| 813 |
+
description: str | None = None,
|
| 814 |
+
tags: set[str] | None = None,
|
| 815 |
+
) -> FunctionPrompt: ...
|
| 816 |
+
|
| 817 |
+
@overload
|
| 818 |
+
def prompt(
|
| 819 |
+
self,
|
| 820 |
+
name_or_fn: str | None = None,
|
| 821 |
+
*,
|
| 822 |
+
name: str | None = None,
|
| 823 |
+
description: str | None = None,
|
| 824 |
+
tags: set[str] | None = None,
|
| 825 |
+
) -> Callable[[AnyFunction], FunctionPrompt]: ...
|
| 826 |
+
|
| 827 |
def prompt(
|
| 828 |
self,
|
| 829 |
name_or_fn: str | AnyFunction | None = None,
|
|
|
|
| 831 |
name: str | None = None,
|
| 832 |
description: str | None = None,
|
| 833 |
tags: set[str] | None = None,
|
| 834 |
+
) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
|
| 835 |
"""Decorator to register a prompt.
|
| 836 |
|
| 837 |
Prompts can optionally request a Context object by adding a parameter with the
|
|
|
|
| 912 |
)
|
| 913 |
self.add_prompt(prompt)
|
| 914 |
|
| 915 |
+
return prompt
|
| 916 |
|
| 917 |
elif isinstance(name_or_fn, str):
|
| 918 |
# Case 3: @prompt("custom_name") - name passed as first argument
|
tests/server/test_server.py
CHANGED
|
@@ -6,7 +6,7 @@ from pydantic import Field
|
|
| 6 |
|
| 7 |
from fastmcp import Client, FastMCP
|
| 8 |
from fastmcp.exceptions import NotFoundError
|
| 9 |
-
from fastmcp.prompts.prompt import Prompt
|
| 10 |
from fastmcp.resources import Resource, ResourceTemplate
|
| 11 |
from fastmcp.server.server import (
|
| 12 |
MountedServer,
|
|
@@ -179,7 +179,6 @@ class TestToolDecorator:
|
|
| 179 |
def __init__(self, x: int):
|
| 180 |
self.x = x
|
| 181 |
|
| 182 |
-
@mcp.tool
|
| 183 |
def add(self, y: int) -> int:
|
| 184 |
return self.x + y
|
| 185 |
|
|
@@ -326,11 +325,11 @@ class TestToolDecorator:
|
|
| 326 |
result_fn = mcp.tool(standalone_function, name="direct_call_tool")
|
| 327 |
|
| 328 |
# The function should be returned unchanged
|
| 329 |
-
assert result_fn
|
| 330 |
|
| 331 |
# Verify the tool was registered correctly
|
| 332 |
tools = await mcp.get_tools()
|
| 333 |
-
assert "direct_call_tool"
|
| 334 |
|
| 335 |
# Verify it can be called
|
| 336 |
result = await mcp._mcp_call_tool("direct_call_tool", {"x": 5, "y": 3})
|
|
@@ -857,11 +856,11 @@ class TestPromptDecorator:
|
|
| 857 |
result_fn = mcp.prompt(standalone_function, name="direct_call_prompt")
|
| 858 |
|
| 859 |
# The function should be returned unchanged
|
| 860 |
-
assert result_fn
|
| 861 |
|
| 862 |
# Verify the prompt was registered correctly
|
| 863 |
prompts = await mcp.get_prompts()
|
| 864 |
-
assert "direct_call_prompt"
|
| 865 |
|
| 866 |
# Verify it can be called
|
| 867 |
async with Client(mcp) as client:
|
|
|
|
| 6 |
|
| 7 |
from fastmcp import Client, FastMCP
|
| 8 |
from fastmcp.exceptions import NotFoundError
|
| 9 |
+
from fastmcp.prompts.prompt import FunctionPrompt, Prompt
|
| 10 |
from fastmcp.resources import Resource, ResourceTemplate
|
| 11 |
from fastmcp.server.server import (
|
| 12 |
MountedServer,
|
|
|
|
| 179 |
def __init__(self, x: int):
|
| 180 |
self.x = x
|
| 181 |
|
|
|
|
| 182 |
def add(self, y: int) -> int:
|
| 183 |
return self.x + y
|
| 184 |
|
|
|
|
| 325 |
result_fn = mcp.tool(standalone_function, name="direct_call_tool")
|
| 326 |
|
| 327 |
# The function should be returned unchanged
|
| 328 |
+
assert isinstance(result_fn, FunctionTool)
|
| 329 |
|
| 330 |
# Verify the tool was registered correctly
|
| 331 |
tools = await mcp.get_tools()
|
| 332 |
+
assert tools["direct_call_tool"] is result_fn
|
| 333 |
|
| 334 |
# Verify it can be called
|
| 335 |
result = await mcp._mcp_call_tool("direct_call_tool", {"x": 5, "y": 3})
|
|
|
|
| 856 |
result_fn = mcp.prompt(standalone_function, name="direct_call_prompt")
|
| 857 |
|
| 858 |
# The function should be returned unchanged
|
| 859 |
+
assert isinstance(result_fn, FunctionPrompt)
|
| 860 |
|
| 861 |
# Verify the prompt was registered correctly
|
| 862 |
prompts = await mcp.get_prompts()
|
| 863 |
+
assert prompts["direct_call_prompt"] is result_fn
|
| 864 |
|
| 865 |
# Verify it can be called
|
| 866 |
async with Client(mcp) as client:
|
tests/server/test_server_interactions.py
CHANGED
|
@@ -936,56 +936,6 @@ class TestResourceTemplates:
|
|
| 936 |
result = await client.read_resource(AnyUrl("resource://test/data"))
|
| 937 |
assert result[0].text == "Data for test" # type: ignore[attr-defined]
|
| 938 |
|
| 939 |
-
async def test_stacked_resource_template_decorators(self):
|
| 940 |
-
"""Test that resource template decorators can be stacked."""
|
| 941 |
-
mcp = FastMCP()
|
| 942 |
-
|
| 943 |
-
@mcp.resource("users://email/{email}")
|
| 944 |
-
@mcp.resource("users://name/{name}")
|
| 945 |
-
def lookup_user(name: str | None = None, email: str | None = None) -> dict:
|
| 946 |
-
if name:
|
| 947 |
-
return {
|
| 948 |
-
"id": "123",
|
| 949 |
-
"name": name,
|
| 950 |
-
"email": "dummy@example.com",
|
| 951 |
-
"lookup": "name",
|
| 952 |
-
}
|
| 953 |
-
elif email:
|
| 954 |
-
return {
|
| 955 |
-
"id": "123",
|
| 956 |
-
"name": "Test User",
|
| 957 |
-
"email": email,
|
| 958 |
-
"lookup": "email",
|
| 959 |
-
}
|
| 960 |
-
else:
|
| 961 |
-
raise ValueError("Either name or email must be provided")
|
| 962 |
-
|
| 963 |
-
# Verify both templates are registered
|
| 964 |
-
templates_dict = await mcp.get_resource_templates()
|
| 965 |
-
templates = list(templates_dict.values())
|
| 966 |
-
assert len(templates) == 2
|
| 967 |
-
template_uris = {t.uri_template for t in templates}
|
| 968 |
-
assert "users://email/{email}" in template_uris
|
| 969 |
-
assert "users://name/{name}" in template_uris
|
| 970 |
-
|
| 971 |
-
# Test lookup by email
|
| 972 |
-
async with Client(mcp) as client:
|
| 973 |
-
email_result = await client.read_resource(
|
| 974 |
-
AnyUrl("users://email/user@example.com")
|
| 975 |
-
)
|
| 976 |
-
assert email_result[0].text # type: ignore[attr-defined]
|
| 977 |
-
email_data = json.loads(email_result[0].text) # type: ignore[attr-defined]
|
| 978 |
-
assert email_data["lookup"] == "email"
|
| 979 |
-
assert email_data["email"] == "user@example.com"
|
| 980 |
-
|
| 981 |
-
# Test lookup by name
|
| 982 |
-
name_result = await client.read_resource(AnyUrl("users://name/John"))
|
| 983 |
-
assert name_result[0].text # type: ignore[attr-defined]
|
| 984 |
-
name_data = json.loads(name_result[0].text) # type: ignore[attr-defined]
|
| 985 |
-
assert name_data["lookup"] == "name"
|
| 986 |
-
assert name_data["name"] == "John"
|
| 987 |
-
assert name_data["email"] == "dummy@example.com"
|
| 988 |
-
|
| 989 |
async def test_template_decorator_with_tags(self):
|
| 990 |
mcp = FastMCP()
|
| 991 |
|
|
|
|
| 936 |
result = await client.read_resource(AnyUrl("resource://test/data"))
|
| 937 |
assert result[0].text == "Data for test" # type: ignore[attr-defined]
|
| 938 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 939 |
async def test_template_decorator_with_tags(self):
|
| 940 |
mcp = FastMCP()
|
| 941 |
|