Jeremiah Lowin commited on
Commit
5a33031
·
unverified ·
2 Parent(s): cb28b5ff6c5553

Merge pull request #711 from jlowin/prompt-decorator

Browse files
README.md CHANGED
@@ -173,10 +173,10 @@ Learn more in the [**Resources & Templates Documentation**](https://gofastmcp.co
173
 
174
  ### Prompts
175
 
176
- Prompts define reusable message templates to guide LLM interactions. Decorate functions with `@mcp.prompt()`. Return strings or `Message` objects.
177
 
178
  ```python
179
- @mcp.prompt()
180
  def summarize_request(text: str) -> str:
181
  """Generate a prompt asking for a summary."""
182
  return f"Please summarize the following text:\n\n{text}"
 
173
 
174
  ### Prompts
175
 
176
+ Prompts define reusable message templates to guide LLM interactions. Decorate functions with `@mcp.prompt`. Return strings or `Message` objects.
177
 
178
  ```python
179
+ @mcp.prompt
180
  def summarize_request(text: str) -> str:
181
  """Generate a prompt asking for a summary."""
182
  return f"Please summarize the following text:\n\n{text}"
docs/servers/context.mdx CHANGED
@@ -71,7 +71,7 @@ async def get_user_profile(user_id: str, ctx: Context) -> dict:
71
  <VersionBadge version="2.2.5" />
72
 
73
  ```python
74
- @mcp.prompt()
75
  async def data_analysis_request(dataset: str, ctx: Context) -> str:
76
  """Generate a request to analyze data with contextual information."""
77
  # Context is available as the ctx parameter
 
71
  <VersionBadge version="2.2.5" />
72
 
73
  ```python
74
+ @mcp.prompt
75
  async def data_analysis_request(dataset: str, ctx: Context) -> str:
76
  """Generate a request to analyze data with contextual information."""
77
  # Context is available as the ctx parameter
docs/servers/fastmcp.mdx CHANGED
@@ -87,7 +87,7 @@ See [Resources & Templates](/servers/resources) for detailed documentation.
87
  Prompts are reusable message templates for guiding the LLM.
88
 
89
  ```python
90
- @mcp.prompt()
91
  def analyze_data(data_points: list[float]) -> str:
92
  """Creates a prompt asking for analysis of numerical data."""
93
  formatted_data = ", ".join(str(point) for point in data_points)
 
87
  Prompts are reusable message templates for guiding the LLM.
88
 
89
  ```python
90
+ @mcp.prompt
91
  def analyze_data(data_points: list[float]) -> str:
92
  """Creates a prompt asking for analysis of numerical data."""
93
  formatted_data = ", ".join(str(point) for point in data_points)
docs/servers/prompts.mdx CHANGED
@@ -33,13 +33,13 @@ from fastmcp.prompts.prompt import Message, PromptMessage, TextContent
33
  mcp = FastMCP(name="PromptServer")
34
 
35
  # Basic prompt returning a string (converted to user message automatically)
36
- @mcp.prompt()
37
  def ask_about_topic(topic: str) -> str:
38
  """Generates a user message asking for an explanation of a topic."""
39
  return f"Can you please explain the concept of '{topic}'?"
40
 
41
  # Prompt returning a specific message type
42
- @mcp.prompt()
43
  def generate_code_request(language: str, task_description: str) -> PromptMessage:
44
  """Generates a user message requesting code generation."""
45
  content = f"Write a {language} function that performs the following task: {task_description}"
@@ -69,7 +69,7 @@ FastMCP intelligently handles different return types from your prompt function:
69
  ```python
70
  from fastmcp.prompts.prompt import Message
71
 
72
- @mcp.prompt()
73
  def roleplay_scenario(character: str, situation: str) -> list[Message]:
74
  """Sets up a roleplaying scenario with initial messages."""
75
  return [
@@ -89,7 +89,7 @@ Type annotations are important for prompts. They:
89
  from pydantic import Field
90
  from typing import Literal, Optional
91
 
92
- @mcp.prompt()
93
  def generate_content_request(
94
  topic: str = Field(description="The main subject to cover"),
95
  format: Literal["blog", "email", "social"] = "blog",
@@ -111,7 +111,7 @@ def generate_content_request(
111
  Parameters in your function signature are considered **required** unless they have a default value.
112
 
113
  ```python
114
- @mcp.prompt()
115
  def data_analysis_prompt(
116
  data_uri: str, # Required - no default value
117
  analysis_type: str = "summary", # Optional - has default value
@@ -154,13 +154,13 @@ FastMCP seamlessly supports both standard (`def`) and asynchronous (`async def`)
154
 
155
  ```python
156
  # Synchronous prompt
157
- @mcp.prompt()
158
  def simple_question(question: str) -> str:
159
  """Generates a simple question to ask the LLM."""
160
  return f"Question: {question}"
161
 
162
  # Asynchronous prompt
163
- @mcp.prompt()
164
  async def data_based_prompt(data_id: str) -> str:
165
  """Generates a prompt based on data that needs to be fetched."""
166
  # In a real scenario, you might fetch data from a database or API
@@ -183,7 +183,7 @@ from fastmcp import FastMCP, Context
183
 
184
  mcp = FastMCP(name="PromptServer")
185
 
186
- @mcp.prompt()
187
  async def generate_report_request(report_type: str, ctx: Context) -> str:
188
  """Generates a request for a report."""
189
  return f"Please create a {report_type} report. Request ID: {ctx.request_id}"
@@ -207,12 +207,12 @@ mcp = FastMCP(
207
  on_duplicate_prompts="error" # Raise an error if a prompt name is duplicated
208
  )
209
 
210
- @mcp.prompt()
211
  def greeting(): return "Hello, how can I help you today?"
212
 
213
  # This registration attempt will raise a ValueError because
214
  # "greeting" is already registered and the behavior is "error".
215
- # @mcp.prompt()
216
  # def greeting(): return "Hi there! What can I do for you?"
217
  ```
218
 
 
33
  mcp = FastMCP(name="PromptServer")
34
 
35
  # Basic prompt returning a string (converted to user message automatically)
36
+ @mcp.prompt
37
  def ask_about_topic(topic: str) -> str:
38
  """Generates a user message asking for an explanation of a topic."""
39
  return f"Can you please explain the concept of '{topic}'?"
40
 
41
  # Prompt returning a specific message type
42
+ @mcp.prompt
43
  def generate_code_request(language: str, task_description: str) -> PromptMessage:
44
  """Generates a user message requesting code generation."""
45
  content = f"Write a {language} function that performs the following task: {task_description}"
 
69
  ```python
70
  from fastmcp.prompts.prompt import Message
71
 
72
+ @mcp.prompt
73
  def roleplay_scenario(character: str, situation: str) -> list[Message]:
74
  """Sets up a roleplaying scenario with initial messages."""
75
  return [
 
89
  from pydantic import Field
90
  from typing import Literal, Optional
91
 
92
+ @mcp.prompt
93
  def generate_content_request(
94
  topic: str = Field(description="The main subject to cover"),
95
  format: Literal["blog", "email", "social"] = "blog",
 
111
  Parameters in your function signature are considered **required** unless they have a default value.
112
 
113
  ```python
114
+ @mcp.prompt
115
  def data_analysis_prompt(
116
  data_uri: str, # Required - no default value
117
  analysis_type: str = "summary", # Optional - has default value
 
154
 
155
  ```python
156
  # Synchronous prompt
157
+ @mcp.prompt
158
  def simple_question(question: str) -> str:
159
  """Generates a simple question to ask the LLM."""
160
  return f"Question: {question}"
161
 
162
  # Asynchronous prompt
163
+ @mcp.prompt
164
  async def data_based_prompt(data_id: str) -> str:
165
  """Generates a prompt based on data that needs to be fetched."""
166
  # In a real scenario, you might fetch data from a database or API
 
183
 
184
  mcp = FastMCP(name="PromptServer")
185
 
186
+ @mcp.prompt
187
  async def generate_report_request(report_type: str, ctx: Context) -> str:
188
  """Generates a request for a report."""
189
  return f"Please create a {report_type} report. Request ID: {ctx.request_id}"
 
207
  on_duplicate_prompts="error" # Raise an error if a prompt name is duplicated
208
  )
209
 
210
+ @mcp.prompt
211
  def greeting(): return "Hello, how can I help you today?"
212
 
213
  # This registration attempt will raise a ValueError because
214
  # "greeting" is already registered and the behavior is "error".
215
+ # @mcp.prompt
216
  # def greeting(): return "Hi there! What can I do for you?"
217
  ```
218
 
src/fastmcp/server/server.py CHANGED
@@ -782,23 +782,33 @@ class FastMCP(Generic[LifespanResultT]):
782
 
783
  def prompt(
784
  self,
 
 
785
  name: str | None = None,
786
  description: str | None = None,
787
  tags: set[str] | None = None,
788
- ) -> Callable[[AnyFunction], AnyFunction]:
789
  """Decorator to register a prompt.
790
 
791
  Prompts can optionally request a Context object by adding a parameter with the
792
  Context type annotation. The context provides access to MCP capabilities like
793
  logging, progress reporting, and session information.
794
 
 
 
 
 
 
 
 
795
  Args:
796
- name: Optional name for the prompt (defaults to function name)
797
  description: Optional description of what the prompt does
798
  tags: Optional set of tags for categorizing the prompt
 
799
 
800
  Example:
801
- @server.prompt()
802
  def analyze_table(table_name: str) -> list[Message]:
803
  schema = read_table_schema(table_name)
804
  return [
@@ -808,7 +818,7 @@ class FastMCP(Generic[LifespanResultT]):
808
  }
809
  ]
810
 
811
- @server.prompt()
812
  def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
813
  ctx.info(f"Analyzing table {table_name}")
814
  schema = read_table_schema(table_name)
@@ -819,8 +829,8 @@ class FastMCP(Generic[LifespanResultT]):
819
  }
820
  ]
821
 
822
- @server.prompt()
823
- async def analyze_file(path: str) -> list[Message]:
824
  content = await read_file(path)
825
  return [
826
  {
@@ -834,26 +844,60 @@ class FastMCP(Generic[LifespanResultT]):
834
  }
835
  }
836
  ]
 
 
 
 
 
 
 
837
  """
838
- # Check if user passed function directly instead of calling decorator
839
- if callable(name):
840
- raise TypeError(
841
- "The @prompt decorator was used incorrectly. "
842
- "Did you forget to call it? Use @prompt() instead of @prompt"
843
- )
844
 
845
- def decorator(fn: AnyFunction) -> AnyFunction:
846
  prompt = Prompt.from_function(
847
  fn=fn,
848
- name=name,
849
  description=description,
850
  tags=tags,
851
  )
852
-
853
  self.add_prompt(prompt)
854
- return DecoratedFunction(fn)
855
 
856
- return decorator
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
857
 
858
  async def run_stdio_async(self) -> None:
859
  """Run the server using stdio transport."""
 
782
 
783
  def prompt(
784
  self,
785
+ name_or_fn: str | AnyFunction | None = None,
786
+ *,
787
  name: str | None = None,
788
  description: str | None = None,
789
  tags: set[str] | None = None,
790
+ ) -> Callable[[AnyFunction], AnyFunction] | AnyFunction:
791
  """Decorator to register a prompt.
792
 
793
  Prompts can optionally request a Context object by adding a parameter with the
794
  Context type annotation. The context provides access to MCP capabilities like
795
  logging, progress reporting, and session information.
796
 
797
+ This decorator supports multiple calling patterns:
798
+ - @server.prompt (without parentheses)
799
+ - @server.prompt (with empty parentheses)
800
+ - @server.prompt("custom_name") (with name as first argument)
801
+ - @server.prompt(name="custom_name") (with name as keyword argument)
802
+ - server.prompt(function, name="custom_name") (direct function call)
803
+
804
  Args:
805
+ name_or_fn: Either a function (when used as @prompt), a string name, or None
806
  description: Optional description of what the prompt does
807
  tags: Optional set of tags for categorizing the prompt
808
+ name: Optional name for the prompt (keyword-only, alternative to name_or_fn)
809
 
810
  Example:
811
+ @server.prompt
812
  def analyze_table(table_name: str) -> list[Message]:
813
  schema = read_table_schema(table_name)
814
  return [
 
818
  }
819
  ]
820
 
821
+ @server.prompt
822
  def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
823
  ctx.info(f"Analyzing table {table_name}")
824
  schema = read_table_schema(table_name)
 
829
  }
830
  ]
831
 
832
+ @server.prompt("custom_name")
833
+ def analyze_file(path: str) -> list[Message]:
834
  content = await read_file(path)
835
  return [
836
  {
 
844
  }
845
  }
846
  ]
847
+
848
+ @server.prompt(name="custom_name")
849
+ def another_prompt(data: str) -> list[Message]:
850
+ return [{"role": "user", "content": data}]
851
+
852
+ # Direct function call
853
+ server.prompt(my_function, name="custom_name")
854
  """
855
+ # Determine the actual name and function based on the calling pattern
856
+ if callable(name_or_fn):
857
+ # Case 1: @prompt (without parens) - function passed directly as decorator
858
+ # Case 2: direct call like prompt(fn, name="something")
859
+ fn = name_or_fn
860
+ prompt_name = name # Use keyword name if provided, otherwise None
861
 
862
+ # Register the prompt immediately
863
  prompt = Prompt.from_function(
864
  fn=fn,
865
+ name=prompt_name,
866
  description=description,
867
  tags=tags,
868
  )
 
869
  self.add_prompt(prompt)
 
870
 
871
+ # If name is provided, this is a direct call, return original function for consistency with tools
872
+ # If name is None, this is @prompt without parens, return DecoratedFunction for proper method handling
873
+ if name is not None:
874
+ return fn # Direct function call
875
+ else:
876
+ return DecoratedFunction(fn) # Decorator usage
877
+
878
+ elif isinstance(name_or_fn, str):
879
+ # Case 3: @prompt("custom_name") - name passed as first argument
880
+ if name is not None:
881
+ raise TypeError(
882
+ "Cannot specify both a name as first argument and as keyword argument. "
883
+ f"Use either @prompt('{name_or_fn}') or @prompt(name='{name}'), not both."
884
+ )
885
+ prompt_name = name_or_fn
886
+ elif name_or_fn is None:
887
+ # Case 4: @prompt() or @prompt(name="something") - use keyword name
888
+ prompt_name = name
889
+ else:
890
+ raise TypeError(
891
+ f"First argument to @prompt must be a function, string, or None, got {type(name_or_fn)}"
892
+ )
893
+
894
+ # Return partial for cases where we need to wait for the function
895
+ return partial(
896
+ self.prompt,
897
+ name=prompt_name,
898
+ description=description,
899
+ tags=tags,
900
+ )
901
 
902
  async def run_stdio_async(self) -> None:
903
  """Run the server using stdio transport."""
tests/client/test_client.py CHANGED
@@ -53,7 +53,7 @@ def fastmcp_server():
53
  return {"id": user_id, "name": f"User {user_id}", "active": True}
54
 
55
  # Add a prompt
56
- @server.prompt()
57
  def welcome(name: str) -> str:
58
  """Example greeting prompt."""
59
  return f"Welcome to FastMCP, {name}!"
 
53
  return {"id": user_id, "name": f"User {user_id}", "active": True}
54
 
55
  # Add a prompt
56
+ @server.prompt
57
  def welcome(name: str) -> str:
58
  """Example greeting prompt."""
59
  return f"Welcome to FastMCP, {name}!"
tests/client/test_sse.py CHANGED
@@ -55,7 +55,7 @@ def fastmcp_server():
55
  return dict(request.headers)
56
 
57
  # Add a prompt
58
- @server.prompt()
59
  def welcome(name: str) -> str:
60
  """Example greeting prompt."""
61
  return f"Welcome to FastMCP, {name}!"
 
55
  return dict(request.headers)
56
 
57
  # Add a prompt
58
+ @server.prompt
59
  def welcome(name: str) -> str:
60
  """Example greeting prompt."""
61
  return f"Welcome to FastMCP, {name}!"
tests/client/test_streamable_http.py CHANGED
@@ -55,7 +55,7 @@ def fastmcp_server():
55
  return dict(request.headers)
56
 
57
  # Add a prompt
58
- @server.prompt()
59
  def welcome(name: str) -> str:
60
  """Example greeting prompt."""
61
  return f"Welcome to FastMCP, {name}!"
 
55
  return dict(request.headers)
56
 
57
  # Add a prompt
58
+ @server.prompt
59
  def welcome(name: str) -> str:
60
  """Example greeting prompt."""
61
  return f"Welcome to FastMCP, {name}!"
tests/deprecated/test_deprecated.py CHANGED
@@ -133,7 +133,7 @@ def test_mount_prompt_separator_deprecation_warning():
133
  main_app.mount("sub", sub_app, prompt_separator="-")
134
 
135
  # Verify the separator is ignored and the default is used
136
- @sub_app.prompt()
137
  def test_prompt():
138
  return "test"
139
 
 
133
  main_app.mount("sub", sub_app, prompt_separator="-")
134
 
135
  # Verify the separator is ignored and the default is used
136
+ @sub_app.prompt
137
  def test_prompt():
138
  return "test"
139
 
tests/deprecated/test_mount_separators.py CHANGED
@@ -53,7 +53,7 @@ def test_mount_prompt_separator_deprecation_warning():
53
  main_app.mount("sub", sub_app, prompt_separator="-")
54
 
55
  # Verify the separator is ignored and the default is used
56
- @sub_app.prompt()
57
  def test_prompt():
58
  return "test"
59
 
 
53
  main_app.mount("sub", sub_app, prompt_separator="-")
54
 
55
  # Verify the separator is ignored and the default is used
56
+ @sub_app.prompt
57
  def test_prompt():
58
  return "test"
59
 
tests/server/http/test_http_dependencies.py CHANGED
@@ -28,7 +28,7 @@ def fastmcp_server():
28
  return dict(request.headers)
29
 
30
  # Add a prompt
31
- @server.prompt()
32
  def get_headers_prompt() -> str:
33
  """Get the HTTP headers from the request."""
34
  request = get_http_request()
 
28
  return dict(request.headers)
29
 
30
  # Add a prompt
31
+ @server.prompt
32
  def get_headers_prompt() -> str:
33
  """Get the HTTP headers from the request."""
34
  request = get_http_request()
tests/server/test_import_server.py CHANGED
@@ -130,7 +130,7 @@ async def test_import_with_prompts():
130
  assistant_app = FastMCP("AssistantApp")
131
 
132
  # Add a prompt to the assistant app
133
- @assistant_app.prompt()
134
  def greeting(name: str) -> str:
135
  return f"Hello, {name}!"
136
 
@@ -174,11 +174,11 @@ async def test_import_multiple_prompts():
174
  sql_app = FastMCP("SQLApp")
175
 
176
  # Add prompts to each app
177
- @python_app.prompt()
178
  def review_python(code: str) -> str:
179
  return f"Reviewing Python code:\n{code}"
180
 
181
- @sql_app.prompt()
182
  def explain_sql(query: str) -> str:
183
  return f"Explaining SQL query:\n{query}"
184
 
@@ -316,7 +316,7 @@ async def test_import_with_proxy_prompts():
316
  main_app = FastMCP("MainApp")
317
  api_app = FastMCP("APIApp")
318
 
319
- @api_app.prompt()
320
  def greeting(name: str) -> str:
321
  """Example greeting prompt."""
322
  return f"Hello, {name} from API!"
 
130
  assistant_app = FastMCP("AssistantApp")
131
 
132
  # Add a prompt to the assistant app
133
+ @assistant_app.prompt
134
  def greeting(name: str) -> str:
135
  return f"Hello, {name}!"
136
 
 
174
  sql_app = FastMCP("SQLApp")
175
 
176
  # Add prompts to each app
177
+ @python_app.prompt
178
  def review_python(code: str) -> str:
179
  return f"Reviewing Python code:\n{code}"
180
 
181
+ @sql_app.prompt
182
  def explain_sql(query: str) -> str:
183
  return f"Explaining SQL query:\n{query}"
184
 
 
316
  main_app = FastMCP("MainApp")
317
  api_app = FastMCP("APIApp")
318
 
319
+ @api_app.prompt
320
  def greeting(name: str) -> str:
321
  """Example greeting prompt."""
322
  return f"Hello, {name} from API!"
tests/server/test_mount.py CHANGED
@@ -194,7 +194,7 @@ class TestMultipleServerMount:
194
  def working_resource():
195
  return "Working resource"
196
 
197
- @working_app.prompt()
198
  def working_prompt() -> str:
199
  return "Working prompt"
200
 
@@ -384,7 +384,7 @@ class TestPrompts:
384
  main_app = FastMCP("MainApp")
385
  assistant_app = FastMCP("AssistantApp")
386
 
387
- @assistant_app.prompt()
388
  def greeting(name: str) -> str:
389
  return f"Hello, {name}!"
390
 
@@ -409,7 +409,7 @@ class TestPrompts:
409
  main_app.mount("assistant", assistant_app)
410
 
411
  # Add a prompt after mounting
412
- @assistant_app.prompt()
413
  def farewell(name: str) -> str:
414
  return f"Goodbye, {name}!"
415
 
@@ -507,7 +507,7 @@ class TestProxyServer:
507
  # Create original server
508
  original_server = FastMCP("OriginalServer")
509
 
510
- @original_server.prompt()
511
  def welcome(name: str) -> str:
512
  return f"Welcome, {name}!"
513
 
 
194
  def working_resource():
195
  return "Working resource"
196
 
197
+ @working_app.prompt
198
  def working_prompt() -> str:
199
  return "Working prompt"
200
 
 
384
  main_app = FastMCP("MainApp")
385
  assistant_app = FastMCP("AssistantApp")
386
 
387
+ @assistant_app.prompt
388
  def greeting(name: str) -> str:
389
  return f"Hello, {name}!"
390
 
 
409
  main_app.mount("assistant", assistant_app)
410
 
411
  # Add a prompt after mounting
412
+ @assistant_app.prompt
413
  def farewell(name: str) -> str:
414
  return f"Goodbye, {name}!"
415
 
 
507
  # Create original server
508
  original_server = FastMCP("OriginalServer")
509
 
510
+ @original_server.prompt
511
  def welcome(name: str) -> str:
512
  return f"Welcome, {name}!"
513
 
tests/server/test_proxy.py CHANGED
@@ -61,7 +61,7 @@ def fastmcp_server():
61
 
62
  # --- Prompts ---
63
 
64
- @server.prompt()
65
  def welcome(name: str) -> str:
66
  return f"Welcome to FastMCP, {name}!"
67
 
 
61
 
62
  # --- Prompts ---
63
 
64
+ @server.prompt
65
  def welcome(name: str) -> str:
66
  return f"Welcome to FastMCP, {name}!"
67
 
tests/server/test_run_server.py CHANGED
@@ -53,7 +53,7 @@
53
 
54
  # # --- Prompts ---
55
 
56
- # @server.prompt()
57
  # def welcome(name: str) -> str:
58
  # return f"Welcome to FastMCP, {name}!"
59
 
 
53
 
54
  # # --- Prompts ---
55
 
56
+ # @server.prompt
57
  # def welcome(name: str) -> str:
58
  # return f"Welcome to FastMCP, {name}!"
59
 
tests/server/test_server.py CHANGED
@@ -659,7 +659,7 @@ class TestPromptDecorator:
659
  async def test_prompt_decorator(self):
660
  mcp = FastMCP()
661
 
662
- @mcp.prompt()
663
  def fn() -> str:
664
  return "Hello, world!"
665
 
@@ -671,16 +671,23 @@ class TestPromptDecorator:
671
  content = await prompt.render()
672
  assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
673
 
674
- async def test_prompt_decorator_incorrect_usage(self):
675
  mcp = FastMCP()
676
 
677
- with pytest.raises(
678
- TypeError, match="The @prompt decorator was used incorrectly"
679
- ):
 
680
 
681
- @mcp.prompt # Missing parentheses #type: ignore
682
- def fn() -> str:
683
- return "Hello, world!"
 
 
 
 
 
 
684
 
685
  async def test_prompt_decorator_with_name(self):
686
  mcp = FastMCP()
@@ -713,7 +720,7 @@ class TestPromptDecorator:
713
  async def test_prompt_decorator_with_parameters(self):
714
  mcp = FastMCP()
715
 
716
- @mcp.prompt()
717
  def test_prompt(name: str, greeting: str = "Hello") -> str:
718
  return f"{greeting}, {name}!"
719
 
@@ -782,7 +789,7 @@ class TestPromptDecorator:
782
 
783
  class MyClass:
784
  @staticmethod
785
- @mcp.prompt()
786
  def test_prompt() -> str:
787
  return "Static Hello, world!"
788
 
@@ -795,7 +802,7 @@ class TestPromptDecorator:
795
  async def test_prompt_decorator_async_function(self):
796
  mcp = FastMCP()
797
 
798
- @mcp.prompt()
799
  async def test_prompt() -> str:
800
  return "Async Hello, world!"
801
 
@@ -818,6 +825,63 @@ class TestPromptDecorator:
818
  prompt = prompts_dict["sample_prompt"]
819
  assert prompt.tags == {"example", "test-tag"}
820
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
821
 
822
  class TestResourcePrefixHelpers:
823
  @pytest.mark.parametrize(
 
659
  async def test_prompt_decorator(self):
660
  mcp = FastMCP()
661
 
662
+ @mcp.prompt
663
  def fn() -> str:
664
  return "Hello, world!"
665
 
 
671
  content = await prompt.render()
672
  assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
673
 
674
+ async def test_prompt_decorator_without_parentheses(self):
675
  mcp = FastMCP()
676
 
677
+ # This should now work correctly (not raise an error)
678
+ @mcp.prompt # No parentheses - this is now supported
679
+ def fn() -> str:
680
+ return "Hello, world!"
681
 
682
+ # Verify the prompt was registered correctly
683
+ prompts = await mcp.get_prompts()
684
+ assert "fn" in prompts
685
+
686
+ # Verify it can be called
687
+ async with Client(mcp) as client:
688
+ result = await client.get_prompt("fn")
689
+ assert len(result.messages) == 1
690
+ assert result.messages[0].content.text == "Hello, world!" # type: ignore[attr-defined]
691
 
692
  async def test_prompt_decorator_with_name(self):
693
  mcp = FastMCP()
 
720
  async def test_prompt_decorator_with_parameters(self):
721
  mcp = FastMCP()
722
 
723
+ @mcp.prompt
724
  def test_prompt(name: str, greeting: str = "Hello") -> str:
725
  return f"{greeting}, {name}!"
726
 
 
789
 
790
  class MyClass:
791
  @staticmethod
792
+ @mcp.prompt
793
  def test_prompt() -> str:
794
  return "Static Hello, world!"
795
 
 
802
  async def test_prompt_decorator_async_function(self):
803
  mcp = FastMCP()
804
 
805
+ @mcp.prompt
806
  async def test_prompt() -> str:
807
  return "Async Hello, world!"
808
 
 
825
  prompt = prompts_dict["sample_prompt"]
826
  assert prompt.tags == {"example", "test-tag"}
827
 
828
+ async def test_prompt_decorator_with_string_name(self):
829
+ """Test that @prompt(\"custom_name\") syntax works correctly."""
830
+ mcp = FastMCP()
831
+
832
+ @mcp.prompt("string_named_prompt")
833
+ def my_function() -> str:
834
+ """A function with a string name."""
835
+ return "Hello from string named prompt!"
836
+
837
+ # Verify the prompt was registered with the custom name
838
+ prompts = await mcp.get_prompts()
839
+ assert "string_named_prompt" in prompts
840
+ assert "my_function" not in prompts # Original name should not be registered
841
+
842
+ # Verify it can be called
843
+ async with Client(mcp) as client:
844
+ result = await client.get_prompt("string_named_prompt")
845
+ assert len(result.messages) == 1
846
+ assert result.messages[0].content.text == "Hello from string named prompt!" # type: ignore[attr-defined]
847
+
848
+ async def test_prompt_direct_function_call(self):
849
+ """Test that prompts can be registered via direct function call."""
850
+ mcp = FastMCP()
851
+
852
+ def standalone_function() -> str:
853
+ """A standalone function to be registered."""
854
+ return "Hello from direct call!"
855
+
856
+ # Register it directly using the new syntax
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:
868
+ result = await client.get_prompt("direct_call_prompt")
869
+ assert len(result.messages) == 1
870
+ assert result.messages[0].content.text == "Hello from direct call!" # type: ignore[attr-defined]
871
+
872
+ async def test_prompt_decorator_conflicting_names_error(self):
873
+ """Test that providing both positional and keyword names raises an error."""
874
+ mcp = FastMCP()
875
+
876
+ with pytest.raises(
877
+ TypeError,
878
+ match="Cannot specify both a name as first argument and as keyword argument",
879
+ ):
880
+
881
+ @mcp.prompt("positional_name", name="keyword_name")
882
+ def my_function() -> str:
883
+ return "Hello, world!"
884
+
885
 
886
  class TestResourcePrefixHelpers:
887
  @pytest.mark.parametrize(
tests/server/test_server_interactions.py CHANGED
@@ -1091,7 +1091,7 @@ class TestPrompts:
1091
  """Test that the prompt decorator registers prompts correctly."""
1092
  mcp = FastMCP()
1093
 
1094
- @mcp.prompt()
1095
  def fn() -> str:
1096
  return "Hello, world!"
1097
 
@@ -1133,20 +1133,23 @@ class TestPrompts:
1133
  content = await prompt.render()
1134
  assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
1135
 
1136
- def test_prompt_decorator_error(self):
1137
- """Test error when decorator is used incorrectly."""
1138
  mcp = FastMCP()
1139
- with pytest.raises(TypeError, match="decorator was used incorrectly"):
1140
 
1141
- @mcp.prompt # type: ignore
1142
- def fn() -> str:
1143
- return "Hello, world!"
 
 
 
 
 
1144
 
1145
  async def test_list_prompts(self):
1146
  """Test listing prompts through MCP protocol."""
1147
  mcp = FastMCP()
1148
 
1149
- @mcp.prompt()
1150
  def fn(name: str, optional: str = "default") -> str:
1151
  return f"Hello, {name}! {optional}"
1152
 
@@ -1169,7 +1172,7 @@ class TestPrompts:
1169
  """Test getting a prompt through MCP protocol."""
1170
  mcp = FastMCP()
1171
 
1172
- @mcp.prompt()
1173
  def fn(name: str) -> str:
1174
  return f"Hello, {name}!"
1175
 
@@ -1185,7 +1188,7 @@ class TestPrompts:
1185
  """Test getting a prompt that returns resource content."""
1186
  mcp = FastMCP()
1187
 
1188
- @mcp.prompt()
1189
  def fn() -> PromptMessage:
1190
  return PromptMessage(
1191
  role="user",
@@ -1220,7 +1223,7 @@ class TestPrompts:
1220
  """Test error when required arguments are missing."""
1221
  mcp = FastMCP()
1222
 
1223
- @mcp.prompt()
1224
  def prompt_fn(name: str) -> str:
1225
  return f"Hello, {name}!"
1226
 
@@ -1271,7 +1274,7 @@ class TestPromptContext:
1271
  async def test_prompt_context(self):
1272
  mcp = FastMCP()
1273
 
1274
- @mcp.prompt()
1275
  def prompt_fn(name: str, ctx: Context) -> str:
1276
  assert isinstance(ctx, Context)
1277
  return f"Hello, {name}! {ctx.request_id}"
 
1091
  """Test that the prompt decorator registers prompts correctly."""
1092
  mcp = FastMCP()
1093
 
1094
+ @mcp.prompt
1095
  def fn() -> str:
1096
  return "Hello, world!"
1097
 
 
1133
  content = await prompt.render()
1134
  assert content[0].content.text == "Hello, world!" # type: ignore[attr-defined]
1135
 
1136
+ async def test_prompt_decorator_with_parens(self):
 
1137
  mcp = FastMCP()
 
1138
 
1139
+ @mcp.prompt()
1140
+ def fn() -> str:
1141
+ return "Hello, world!"
1142
+
1143
+ prompts_dict = await mcp.get_prompts()
1144
+ assert len(prompts_dict) == 1
1145
+ prompt = prompts_dict["fn"]
1146
+ assert prompt.name == "fn"
1147
 
1148
  async def test_list_prompts(self):
1149
  """Test listing prompts through MCP protocol."""
1150
  mcp = FastMCP()
1151
 
1152
+ @mcp.prompt
1153
  def fn(name: str, optional: str = "default") -> str:
1154
  return f"Hello, {name}! {optional}"
1155
 
 
1172
  """Test getting a prompt through MCP protocol."""
1173
  mcp = FastMCP()
1174
 
1175
+ @mcp.prompt
1176
  def fn(name: str) -> str:
1177
  return f"Hello, {name}!"
1178
 
 
1188
  """Test getting a prompt that returns resource content."""
1189
  mcp = FastMCP()
1190
 
1191
+ @mcp.prompt
1192
  def fn() -> PromptMessage:
1193
  return PromptMessage(
1194
  role="user",
 
1223
  """Test error when required arguments are missing."""
1224
  mcp = FastMCP()
1225
 
1226
+ @mcp.prompt
1227
  def prompt_fn(name: str) -> str:
1228
  return f"Hello, {name}!"
1229
 
 
1274
  async def test_prompt_context(self):
1275
  mcp = FastMCP()
1276
 
1277
+ @mcp.prompt
1278
  def prompt_fn(name: str, ctx: Context) -> str:
1279
  assert isinstance(ctx, Context)
1280
  return f"Hello, {name}! {ctx.request_id}"
tests/test_servers/fastmcp_server.py CHANGED
@@ -53,6 +53,6 @@ async def get_user(user_id: str) -> dict[str, Any] | None:
53
  # --- Prompts ---
54
 
55
 
56
- @server.prompt()
57
  def welcome(name: str) -> str:
58
  return f"Welcome to FastMCP, {name}!"
 
53
  # --- Prompts ---
54
 
55
 
56
+ @server.prompt
57
  def welcome(name: str) -> str:
58
  return f"Welcome to FastMCP, {name}!"