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], AnyFunction] | 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 function
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 fn
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], 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) -> 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], AnyFunction] | 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 fn
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 is standalone_function
330
 
331
  # Verify the tool was registered correctly
332
  tools = await mcp.get_tools()
333
- assert "direct_call_tool" in tools
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 is standalone_function
861
 
862
  # Verify the prompt was registered correctly
863
  prompts = await mcp.get_prompts()
864
- assert "direct_call_prompt" in prompts
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