fastmcp 2.6.1__py3-none-any.whl → 2.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fastmcp/server/server.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import datetime
6
+ import inspect
6
7
  import re
7
8
  import warnings
8
9
  from collections.abc import AsyncIterator, Awaitable, Callable
@@ -13,7 +14,7 @@ from contextlib import (
13
14
  )
14
15
  from functools import partial
15
16
  from pathlib import Path
16
- from typing import TYPE_CHECKING, Any, Generic, Literal
17
+ from typing import TYPE_CHECKING, Any, Generic, Literal, overload
17
18
 
18
19
  import anyio
19
20
  import httpx
@@ -44,7 +45,7 @@ import fastmcp.server
44
45
  import fastmcp.settings
45
46
  from fastmcp.exceptions import NotFoundError
46
47
  from fastmcp.prompts import Prompt, PromptManager
47
- from fastmcp.prompts.prompt import PromptResult
48
+ from fastmcp.prompts.prompt import FunctionPrompt
48
49
  from fastmcp.resources import Resource, ResourceManager
49
50
  from fastmcp.resources.template import ResourceTemplate
50
51
  from fastmcp.server.auth.auth import OAuthProvider
@@ -55,9 +56,8 @@ from fastmcp.server.http import (
55
56
  create_streamable_http_app,
56
57
  )
57
58
  from fastmcp.tools import ToolManager
58
- from fastmcp.tools.tool import Tool
59
+ from fastmcp.tools.tool import FunctionTool, Tool
59
60
  from fastmcp.utilities.cache import TimedCache
60
- from fastmcp.utilities.decorators import DecoratedFunction
61
61
  from fastmcp.utilities.logging import get_logger
62
62
  from fastmcp.utilities.mcp_config import MCPConfig
63
63
 
@@ -154,7 +154,6 @@ class FastMCP(Generic[LifespanResultT]):
154
154
  self._additional_http_routes: list[BaseRoute] = []
155
155
  self._tool_manager = ToolManager(
156
156
  duplicate_behavior=on_duplicate_tools,
157
- serializer=tool_serializer,
158
157
  mask_error_details=self.settings.mask_error_details,
159
158
  )
160
159
  self._resource_manager = ResourceManager(
@@ -165,6 +164,7 @@ class FastMCP(Generic[LifespanResultT]):
165
164
  duplicate_behavior=on_duplicate_prompts,
166
165
  mask_error_details=self.settings.mask_error_details,
167
166
  )
167
+ self._tool_serializer = tool_serializer
168
168
 
169
169
  if lifespan is None:
170
170
  self._has_lifespan = False
@@ -183,10 +183,9 @@ class FastMCP(Generic[LifespanResultT]):
183
183
 
184
184
  if tools:
185
185
  for tool in tools:
186
- if isinstance(tool, Tool):
187
- self._tool_manager.add_tool(tool)
188
- else:
189
- self.add_tool(tool)
186
+ if not isinstance(tool, Tool):
187
+ tool = Tool.from_function(tool, serializer=self._tool_serializer)
188
+ self.add_tool(tool)
190
189
 
191
190
  # Set up MCP protocol handlers
192
191
  self._setup_handlers()
@@ -349,18 +348,18 @@ class FastMCP(Generic[LifespanResultT]):
349
348
  """
350
349
 
351
350
  def decorator(
352
- func: Callable[[Request], Awaitable[Response]],
351
+ fn: Callable[[Request], Awaitable[Response]],
353
352
  ) -> Callable[[Request], Awaitable[Response]]:
354
353
  self._additional_http_routes.append(
355
354
  Route(
356
355
  path,
357
- endpoint=func,
356
+ endpoint=fn,
358
357
  methods=methods,
359
358
  name=name,
360
359
  include_in_schema=include_in_schema,
361
360
  )
362
361
  )
363
- return func
362
+ return fn
364
363
 
365
364
  return decorator
366
365
 
@@ -484,15 +483,7 @@ class FastMCP(Generic[LifespanResultT]):
484
483
 
485
484
  raise NotFoundError(f"Unknown prompt: {name}")
486
485
 
487
- def add_tool(
488
- self,
489
- fn: AnyFunction,
490
- name: str | None = None,
491
- description: str | None = None,
492
- tags: set[str] | None = None,
493
- annotations: ToolAnnotations | dict[str, Any] | None = None,
494
- exclude_args: list[str] | None = None,
495
- ) -> None:
486
+ def add_tool(self, tool: Tool) -> None:
496
487
  """Add a tool to the server.
497
488
 
498
489
  The tool function can optionally request a Context object by adding a parameter
@@ -505,17 +496,7 @@ class FastMCP(Generic[LifespanResultT]):
505
496
  tags: Optional set of tags for categorizing the tool
506
497
  annotations: Optional annotations about the tool's behavior
507
498
  """
508
- if isinstance(annotations, dict):
509
- annotations = ToolAnnotations(**annotations)
510
-
511
- self._tool_manager.add_tool_from_fn(
512
- fn,
513
- name=name,
514
- description=description,
515
- tags=tags,
516
- annotations=annotations,
517
- exclude_args=exclude_args,
518
- )
499
+ self._tool_manager.add_tool(tool)
519
500
  self._cache.clear()
520
501
 
521
502
  def remove_tool(self, name: str) -> None:
@@ -530,61 +511,141 @@ class FastMCP(Generic[LifespanResultT]):
530
511
  self._tool_manager.remove_tool(name)
531
512
  self._cache.clear()
532
513
 
514
+ @overload
533
515
  def tool(
534
516
  self,
517
+ name_or_fn: AnyFunction,
518
+ *,
535
519
  name: str | None = None,
536
520
  description: str | None = None,
537
521
  tags: set[str] | None = None,
538
522
  annotations: ToolAnnotations | dict[str, Any] | None = None,
539
523
  exclude_args: list[str] | None = None,
540
- ) -> Callable[[AnyFunction], AnyFunction]:
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,
541
+ *,
542
+ name: str | None = None,
543
+ description: str | 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:
541
548
  """Decorator to register a tool.
542
549
 
543
550
  Tools can optionally request a Context object by adding a parameter with the
544
551
  Context type annotation. The context provides access to MCP capabilities like
545
552
  logging, progress reporting, and resource access.
546
553
 
554
+ This decorator supports multiple calling patterns:
555
+ - @server.tool (without parentheses)
556
+ - @server.tool (with empty parentheses)
557
+ - @server.tool("custom_name") (with name as first argument)
558
+ - @server.tool(name="custom_name") (with name as keyword argument)
559
+ - server.tool(function, name="custom_name") (direct function call)
560
+
547
561
  Args:
548
- name: Optional name for the tool (defaults to function name)
562
+ name_or_fn: Either a function (when used as @tool), a string name, or None
549
563
  description: Optional description of what the tool does
550
564
  tags: Optional set of tags for categorizing the tool
551
565
  annotations: Optional annotations about the tool's behavior
566
+ exclude_args: Optional list of argument names to exclude from the tool schema
567
+ name: Optional name for the tool (keyword-only, alternative to name_or_fn)
552
568
 
553
569
  Example:
554
- @server.tool()
570
+ @server.tool
555
571
  def my_tool(x: int) -> str:
556
572
  return str(x)
557
573
 
558
- @server.tool()
559
- def tool_with_context(x: int, ctx: Context) -> str:
560
- ctx.info(f"Processing {x}")
574
+ @server.tool
575
+ def my_tool(x: int) -> str:
561
576
  return str(x)
562
577
 
563
- @server.tool()
564
- async def async_tool(x: int, context: Context) -> str:
565
- await context.report_progress(50, 100)
578
+ @server.tool("custom_name")
579
+ def my_tool(x: int) -> str:
566
580
  return str(x)
581
+
582
+ @server.tool(name="custom_name")
583
+ def my_tool(x: int) -> str:
584
+ return str(x)
585
+
586
+ # Direct function call
587
+ server.tool(my_function, name="custom_name")
567
588
  """
589
+ if isinstance(annotations, dict):
590
+ annotations = ToolAnnotations(**annotations)
568
591
 
569
- # Check if user passed function directly instead of calling decorator
570
- if callable(name):
571
- raise TypeError(
572
- "The @tool decorator was used incorrectly. "
573
- "Did you forget to call it? Use @tool() instead of @tool"
592
+ if isinstance(name_or_fn, classmethod):
593
+ raise ValueError(
594
+ inspect.cleandoc(
595
+ """
596
+ To decorate a classmethod, first define the method and then call
597
+ tool() directly on the method instead of using it as a
598
+ decorator. See https://gofastmcp.com/patterns/decorating-methods
599
+ for examples and more information.
600
+ """
601
+ )
574
602
  )
575
603
 
576
- def decorator(fn: AnyFunction) -> AnyFunction:
577
- self.add_tool(
604
+ # Determine the actual name and function based on the calling pattern
605
+ if inspect.isroutine(name_or_fn):
606
+ # Case 1: @tool (without parens) - function passed directly
607
+ # Case 2: direct call like tool(fn, name="something")
608
+ fn = name_or_fn
609
+ tool_name = name # Use keyword name if provided, otherwise None
610
+
611
+ # Register the tool immediately and return the tool object
612
+ tool = Tool.from_function(
578
613
  fn,
579
- name=name,
614
+ name=tool_name,
580
615
  description=description,
581
616
  tags=tags,
582
617
  annotations=annotations,
583
618
  exclude_args=exclude_args,
619
+ serializer=self._tool_serializer,
620
+ )
621
+ self.add_tool(tool)
622
+ return tool
623
+
624
+ elif isinstance(name_or_fn, str):
625
+ # Case 3: @tool("custom_name") - name passed as first argument
626
+ if name is not None:
627
+ raise TypeError(
628
+ "Cannot specify both a name as first argument and as keyword argument. "
629
+ f"Use either @tool('{name_or_fn}') or @tool(name='{name}'), not both."
630
+ )
631
+ tool_name = name_or_fn
632
+ elif name_or_fn is None:
633
+ # Case 4: @tool or @tool(name="something") - use keyword name
634
+ tool_name = name
635
+ else:
636
+ raise TypeError(
637
+ f"First argument to @tool must be a function, string, or None, got {type(name_or_fn)}"
584
638
  )
585
- return fn
586
639
 
587
- return decorator
640
+ # Return partial for cases where we need to wait for the function
641
+ return partial(
642
+ self.tool,
643
+ name=tool_name,
644
+ description=description,
645
+ tags=tags,
646
+ annotations=annotations,
647
+ exclude_args=exclude_args,
648
+ )
588
649
 
589
650
  def add_resource(self, resource: Resource, key: str | None = None) -> None:
590
651
  """Add a resource to the server.
@@ -596,6 +657,14 @@ class FastMCP(Generic[LifespanResultT]):
596
657
  self._resource_manager.add_resource(resource, key=key)
597
658
  self._cache.clear()
598
659
 
660
+ def add_template(self, template: ResourceTemplate, key: str | None = None) -> None:
661
+ """Add a resource template to the server.
662
+
663
+ Args:
664
+ template: A ResourceTemplate instance to add
665
+ """
666
+ self._resource_manager.add_template(template, key=key)
667
+
599
668
  def add_resource_fn(
600
669
  self,
601
670
  fn: AnyFunction,
@@ -618,6 +687,12 @@ class FastMCP(Generic[LifespanResultT]):
618
687
  mime_type: Optional MIME type for the resource
619
688
  tags: Optional set of tags for categorizing the resource
620
689
  """
690
+ # deprecated since 2.7.0
691
+ warnings.warn(
692
+ "The add_resource_fn method is deprecated. Use the resource decorator instead.",
693
+ DeprecationWarning,
694
+ stacklevel=2,
695
+ )
621
696
  self._resource_manager.add_resource_or_template_from_fn(
622
697
  fn=fn,
623
698
  uri=uri,
@@ -636,7 +711,7 @@ class FastMCP(Generic[LifespanResultT]):
636
711
  description: str | None = None,
637
712
  mime_type: str | None = None,
638
713
  tags: set[str] | None = None,
639
- ) -> Callable[[AnyFunction], AnyFunction]:
714
+ ) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
640
715
  """Decorator to register a function as a resource.
641
716
 
642
717
  The function will be called when the resource is read to generate its content.
@@ -684,64 +759,124 @@ class FastMCP(Generic[LifespanResultT]):
684
759
  return f"Weather for {city}: {data}"
685
760
  """
686
761
  # Check if user passed function directly instead of calling decorator
687
- if callable(uri):
762
+ if inspect.isroutine(uri):
688
763
  raise TypeError(
689
764
  "The @resource decorator was used incorrectly. "
690
765
  "Did you forget to call it? Use @resource('uri') instead of @resource"
691
766
  )
692
767
 
693
- def decorator(fn: AnyFunction) -> AnyFunction:
694
- self.add_resource_fn(
695
- fn=fn,
696
- uri=uri,
697
- name=name,
698
- description=description,
699
- mime_type=mime_type,
700
- tags=tags,
768
+ def decorator(fn: AnyFunction) -> Resource | ResourceTemplate:
769
+ from fastmcp.server.context import Context
770
+
771
+ if isinstance(fn, classmethod): # type: ignore[reportUnnecessaryIsInstance]
772
+ raise ValueError(
773
+ inspect.cleandoc(
774
+ """
775
+ To decorate a classmethod, first define the method and then call
776
+ resource() directly on the method instead of using it as a
777
+ decorator. See https://gofastmcp.com/patterns/decorating-methods
778
+ for examples and more information.
779
+ """
780
+ )
781
+ )
782
+
783
+ # Check if this should be a template
784
+ has_uri_params = "{" in uri and "}" in uri
785
+ # check if the function has any parameters (other than injected context)
786
+ has_func_params = any(
787
+ p
788
+ for p in inspect.signature(fn).parameters.values()
789
+ if p.annotation is not Context
701
790
  )
702
- return fn
791
+
792
+ if has_uri_params or has_func_params:
793
+ template = ResourceTemplate.from_function(
794
+ fn=fn,
795
+ uri_template=uri,
796
+ name=name,
797
+ description=description,
798
+ mime_type=mime_type,
799
+ tags=tags,
800
+ )
801
+ self.add_template(template)
802
+ return template
803
+ elif not has_uri_params and not has_func_params:
804
+ resource = Resource.from_function(
805
+ fn=fn,
806
+ uri=uri,
807
+ name=name,
808
+ description=description,
809
+ mime_type=mime_type,
810
+ tags=tags,
811
+ )
812
+ self.add_resource(resource)
813
+ return resource
814
+ else:
815
+ raise ValueError(
816
+ "Invalid resource or template definition due to a "
817
+ "mismatch between URI parameters and function parameters."
818
+ )
703
819
 
704
820
  return decorator
705
821
 
706
- def add_prompt(
707
- self,
708
- fn: Callable[..., PromptResult | Awaitable[PromptResult]],
709
- name: str | None = None,
710
- description: str | None = None,
711
- tags: set[str] | None = None,
712
- ) -> None:
822
+ def add_prompt(self, prompt: Prompt) -> None:
713
823
  """Add a prompt to the server.
714
824
 
715
825
  Args:
716
826
  prompt: A Prompt instance to add
717
827
  """
718
- self._prompt_manager.add_prompt_from_fn(
719
- fn=fn,
720
- name=name,
721
- description=description,
722
- tags=tags,
723
- )
828
+ self._prompt_manager.add_prompt(prompt)
724
829
  self._cache.clear()
725
830
 
831
+ @overload
726
832
  def prompt(
727
833
  self,
834
+ name_or_fn: AnyFunction,
835
+ *,
728
836
  name: str | None = None,
729
837
  description: str | None = None,
730
838
  tags: set[str] | None = None,
731
- ) -> Callable[[AnyFunction], AnyFunction]:
839
+ ) -> FunctionPrompt: ...
840
+
841
+ @overload
842
+ def prompt(
843
+ self,
844
+ name_or_fn: str | None = None,
845
+ *,
846
+ name: str | None = None,
847
+ description: str | None = None,
848
+ tags: set[str] | None = None,
849
+ ) -> Callable[[AnyFunction], FunctionPrompt]: ...
850
+
851
+ def prompt(
852
+ self,
853
+ name_or_fn: str | AnyFunction | None = None,
854
+ *,
855
+ name: str | None = None,
856
+ description: str | None = None,
857
+ tags: set[str] | None = None,
858
+ ) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
732
859
  """Decorator to register a prompt.
733
860
 
734
861
  Prompts can optionally request a Context object by adding a parameter with the
735
862
  Context type annotation. The context provides access to MCP capabilities like
736
863
  logging, progress reporting, and session information.
737
864
 
865
+ This decorator supports multiple calling patterns:
866
+ - @server.prompt (without parentheses)
867
+ - @server.prompt (with empty parentheses)
868
+ - @server.prompt("custom_name") (with name as first argument)
869
+ - @server.prompt(name="custom_name") (with name as keyword argument)
870
+ - server.prompt(function, name="custom_name") (direct function call)
871
+
738
872
  Args:
739
- name: Optional name for the prompt (defaults to function name)
873
+ name_or_fn: Either a function (when used as @prompt), a string name, or None
740
874
  description: Optional description of what the prompt does
741
875
  tags: Optional set of tags for categorizing the prompt
876
+ name: Optional name for the prompt (keyword-only, alternative to name_or_fn)
742
877
 
743
878
  Example:
744
- @server.prompt()
879
+ @server.prompt
745
880
  def analyze_table(table_name: str) -> list[Message]:
746
881
  schema = read_table_schema(table_name)
747
882
  return [
@@ -751,7 +886,7 @@ class FastMCP(Generic[LifespanResultT]):
751
886
  }
752
887
  ]
753
888
 
754
- @server.prompt()
889
+ @server.prompt
755
890
  def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
756
891
  ctx.info(f"Analyzing table {table_name}")
757
892
  schema = read_table_schema(table_name)
@@ -762,8 +897,8 @@ class FastMCP(Generic[LifespanResultT]):
762
897
  }
763
898
  ]
764
899
 
765
- @server.prompt()
766
- async def analyze_file(path: str) -> list[Message]:
900
+ @server.prompt("custom_name")
901
+ def analyze_file(path: str) -> list[Message]:
767
902
  content = await read_file(path)
768
903
  return [
769
904
  {
@@ -777,19 +912,68 @@ class FastMCP(Generic[LifespanResultT]):
777
912
  }
778
913
  }
779
914
  ]
915
+
916
+ @server.prompt(name="custom_name")
917
+ def another_prompt(data: str) -> list[Message]:
918
+ return [{"role": "user", "content": data}]
919
+
920
+ # Direct function call
921
+ server.prompt(my_function, name="custom_name")
780
922
  """
781
- # Check if user passed function directly instead of calling decorator
782
- if callable(name):
783
- raise TypeError(
784
- "The @prompt decorator was used incorrectly. "
785
- "Did you forget to call it? Use @prompt() instead of @prompt"
923
+
924
+ if isinstance(name_or_fn, classmethod):
925
+ raise ValueError(
926
+ inspect.cleandoc(
927
+ """
928
+ To decorate a classmethod, first define the method and then call
929
+ prompt() directly on the method instead of using it as a
930
+ decorator. See https://gofastmcp.com/patterns/decorating-methods
931
+ for examples and more information.
932
+ """
933
+ )
786
934
  )
787
935
 
788
- def decorator(func: AnyFunction) -> AnyFunction:
789
- self.add_prompt(func, name=name, description=description, tags=tags)
790
- return DecoratedFunction(func)
936
+ # Determine the actual name and function based on the calling pattern
937
+ if inspect.isroutine(name_or_fn):
938
+ # Case 1: @prompt (without parens) - function passed directly as decorator
939
+ # Case 2: direct call like prompt(fn, name="something")
940
+ fn = name_or_fn
941
+ prompt_name = name # Use keyword name if provided, otherwise None
791
942
 
792
- return decorator
943
+ # Register the prompt immediately
944
+ prompt = Prompt.from_function(
945
+ fn=fn,
946
+ name=prompt_name,
947
+ description=description,
948
+ tags=tags,
949
+ )
950
+ self.add_prompt(prompt)
951
+
952
+ return prompt
953
+
954
+ elif isinstance(name_or_fn, str):
955
+ # Case 3: @prompt("custom_name") - name passed as first argument
956
+ if name is not None:
957
+ raise TypeError(
958
+ "Cannot specify both a name as first argument and as keyword argument. "
959
+ f"Use either @prompt('{name_or_fn}') or @prompt(name='{name}'), not both."
960
+ )
961
+ prompt_name = name_or_fn
962
+ elif name_or_fn is None:
963
+ # Case 4: @prompt() or @prompt(name="something") - use keyword name
964
+ prompt_name = name
965
+ else:
966
+ raise TypeError(
967
+ f"First argument to @prompt must be a function, string, or None, got {type(name_or_fn)}"
968
+ )
969
+
970
+ # Return partial for cases where we need to wait for the function
971
+ return partial(
972
+ self.prompt,
973
+ name=prompt_name,
974
+ description=description,
975
+ tags=tags,
976
+ )
793
977
 
794
978
  async def run_stdio_async(self) -> None:
795
979
  """Run the server using stdio transport."""
fastmcp/tools/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .tool import Tool
1
+ from .tool import Tool, FunctionTool
2
2
  from .tool_manager import ToolManager
3
3
 
4
- __all__ = ["Tool", "ToolManager"]
4
+ __all__ = ["Tool", "ToolManager", "FunctionTool"]