Jeremiah Lowin commited on
Commit
43369c9
·
unverified ·
2 Parent(s): 85ac398c9edd57

Merge pull request #908 from jlowin/feature/server-side-type-conversion

Browse files
docs/servers/prompts.mdx CHANGED
@@ -57,6 +57,84 @@ def generate_code_request(language: str, task_description: str) -> PromptMessage
57
  Functions with `*args` or `**kwargs` are not supported as prompts. This restriction exists because FastMCP needs to generate a complete parameter schema for the MCP protocol, which isn't possible with variable argument lists.
58
  </Tip>
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  ### Return Values
61
 
62
  FastMCP intelligently handles different return types from your prompt function:
@@ -78,33 +156,6 @@ def roleplay_scenario(character: str, situation: str) -> list[Message]:
78
  ]
79
  ```
80
 
81
- ### Type Annotations
82
-
83
- Type annotations are important for prompts. They:
84
- 1. Inform FastMCP about the expected types for each parameter.
85
- 2. Allow validation of parameters received from clients.
86
- 3. Are used to generate the prompt's schema for the MCP protocol.
87
-
88
- ```python
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",
96
- tone: str = "professional",
97
- word_count: Optional[int] = None
98
- ) -> str:
99
- """Create a request for generating content in a specific format."""
100
- prompt = f"Please write a {format} post about {topic} in a {tone} tone."
101
-
102
- if word_count:
103
- prompt += f" It should be approximately {word_count} words long."
104
-
105
- return prompt
106
- ```
107
-
108
 
109
  ### Required vs. Optional Parameters
110
 
 
57
  Functions with `*args` or `**kwargs` are not supported as prompts. This restriction exists because FastMCP needs to generate a complete parameter schema for the MCP protocol, which isn't possible with variable argument lists.
58
  </Tip>
59
 
60
+ ### Argument Types
61
+
62
+ <VersionBadge version="2.9.0" />
63
+
64
+ The MCP specification requires that all prompt arguments be passed as strings, but FastMCP allows you to use typed annotations for better developer experience. When you use complex types like `list[int]` or `dict[str, str]`, FastMCP:
65
+
66
+ 1. **Automatically converts** string arguments from MCP clients to the expected types
67
+ 2. **Generates helpful descriptions** showing the exact JSON string format needed
68
+ 3. **Preserves direct usage** - you can still call prompts with properly typed arguments
69
+
70
+ Since the MCP specification only allows string arguments, clients need to know what string format to use for complex types. FastMCP solves this by automatically enhancing the argument descriptions with JSON schema information, making it clear to both humans and LLMs how to format their arguments.
71
+
72
+ <CodeGroup>
73
+
74
+ ```python Python Code
75
+ @mcp.prompt
76
+ def analyze_data(
77
+ numbers: list[int],
78
+ metadata: dict[str, str],
79
+ threshold: float
80
+ ) -> str:
81
+ """Analyze numerical data."""
82
+ avg = sum(numbers) / len(numbers)
83
+ return f"Average: {avg}, above threshold: {avg > threshold}"
84
+ ```
85
+
86
+ ```json Resulting MCP Prompt
87
+ {
88
+ "name": "analyze_data",
89
+ "description": "Analyze numerical data.",
90
+ "arguments": [
91
+ {
92
+ "name": "numbers",
93
+ "description": "Provide as a JSON string matching the following schema: {\"items\":{\"type\":\"integer\"},\"type\":\"array\"}",
94
+ "required": true
95
+ },
96
+ {
97
+ "name": "metadata",
98
+ "description": "Provide as a JSON string matching the following schema: {\"additionalProperties\":{\"type\":\"string\"},\"type\":\"object\"}",
99
+ "required": true
100
+ },
101
+ {
102
+ "name": "threshold",
103
+ "description": "Provide as a JSON string matching the following schema: {\"type\":\"number\"}",
104
+ "required": true
105
+ }
106
+ ]
107
+ }
108
+ ```
109
+
110
+ </CodeGroup>
111
+
112
+ **MCP clients will call this prompt with string arguments:**
113
+ ```json
114
+ {
115
+ "numbers": "[1, 2, 3, 4, 5]",
116
+ "metadata": "{\"source\": \"api\", \"version\": \"1.0\"}",
117
+ "threshold": "2.5"
118
+ }
119
+ ```
120
+
121
+ **But you can still call it directly with proper types:**
122
+ ```python
123
+ # This also works for direct calls
124
+ result = await prompt.render({
125
+ "numbers": [1, 2, 3, 4, 5],
126
+ "metadata": {"source": "api", "version": "1.0"},
127
+ "threshold": 2.5
128
+ })
129
+ ```
130
+
131
+ <Warning>
132
+ Keep your type annotations simple when using this feature. Complex nested types or custom classes may not convert reliably from JSON strings. The automatically generated schema descriptions are the only guidance users receive about the expected format.
133
+
134
+ Good choices: `list[int]`, `dict[str, str]`, `float`, `bool`
135
+ Avoid: Complex Pydantic models, deeply nested structures, custom classes
136
+ </Warning>
137
+
138
  ### Return Values
139
 
140
  FastMCP intelligently handles different return types from your prompt function:
 
156
  ]
157
  ```
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
  ### Required vs. Optional Parameters
161
 
src/fastmcp/prompts/prompt.py CHANGED
@@ -3,15 +3,16 @@
3
  from __future__ import annotations as _annotations
4
 
5
  import inspect
 
6
  from abc import ABC, abstractmethod
7
  from collections.abc import Awaitable, Callable, Sequence
8
- from typing import TYPE_CHECKING, Any
9
 
10
  import pydantic_core
11
  from mcp.types import Prompt as MCPPrompt
12
  from mcp.types import PromptArgument as MCPPromptArgument
13
  from mcp.types import PromptMessage, Role, TextContent
14
- from pydantic import Field, TypeAdapter, validate_call
15
 
16
  from fastmcp.exceptions import PromptError
17
  from fastmcp.server.dependencies import get_context
@@ -25,10 +26,6 @@ from fastmcp.utilities.types import (
25
  get_cached_typeadapter,
26
  )
27
 
28
- if TYPE_CHECKING:
29
- pass
30
-
31
-
32
  logger = get_logger(__name__)
33
 
34
 
@@ -181,17 +178,43 @@ class FunctionPrompt(Prompt):
181
  arguments: list[PromptArgument] = []
182
  if "properties" in parameters:
183
  for param_name, param in parameters["properties"].items():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  arguments.append(
185
  PromptArgument(
186
  name=param_name,
187
- description=param.get("description"),
188
  required=param_name in parameters.get("required", []),
189
  )
190
  )
191
 
192
- # ensure the arguments are properly cast
193
- fn = validate_call(fn)
194
-
195
  return cls(
196
  name=func_name,
197
  description=description,
@@ -201,6 +224,60 @@ class FunctionPrompt(Prompt):
201
  fn=fn,
202
  )
203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  async def render(
205
  self,
206
  arguments: dict[str, Any] | None = None,
@@ -223,6 +300,9 @@ class FunctionPrompt(Prompt):
223
  if context_kwarg and context_kwarg not in kwargs:
224
  kwargs[context_kwarg] = get_context()
225
 
 
 
 
226
  # Call function and check if result is a coroutine
227
  result = self.fn(**kwargs)
228
  if inspect.iscoroutine(result):
 
3
  from __future__ import annotations as _annotations
4
 
5
  import inspect
6
+ import json
7
  from abc import ABC, abstractmethod
8
  from collections.abc import Awaitable, Callable, Sequence
9
+ from typing import Any
10
 
11
  import pydantic_core
12
  from mcp.types import Prompt as MCPPrompt
13
  from mcp.types import PromptArgument as MCPPromptArgument
14
  from mcp.types import PromptMessage, Role, TextContent
15
+ from pydantic import Field, TypeAdapter
16
 
17
  from fastmcp.exceptions import PromptError
18
  from fastmcp.server.dependencies import get_context
 
26
  get_cached_typeadapter,
27
  )
28
 
 
 
 
 
29
  logger = get_logger(__name__)
30
 
31
 
 
178
  arguments: list[PromptArgument] = []
179
  if "properties" in parameters:
180
  for param_name, param in parameters["properties"].items():
181
+ arg_description = param.get("description")
182
+
183
+ # For non-string parameters, append JSON schema info to help users
184
+ # understand the expected format when passing as strings (MCP requirement)
185
+ if param_name in sig.parameters:
186
+ sig_param = sig.parameters[param_name]
187
+ if (
188
+ sig_param.annotation != inspect.Parameter.empty
189
+ and sig_param.annotation is not str
190
+ and param_name != context_kwarg
191
+ ):
192
+ # Get the JSON schema for this specific parameter type
193
+ try:
194
+ param_adapter = get_cached_typeadapter(sig_param.annotation)
195
+ param_schema = param_adapter.json_schema()
196
+
197
+ # Create compact schema representation
198
+ schema_str = json.dumps(param_schema, separators=(",", ":"))
199
+
200
+ # Append schema info to description
201
+ schema_note = f"Provide as a JSON string matching the following schema: {schema_str}"
202
+ if arg_description:
203
+ arg_description = f"{arg_description}\n\n{schema_note}"
204
+ else:
205
+ arg_description = schema_note
206
+ except Exception:
207
+ # If schema generation fails, skip enhancement
208
+ pass
209
+
210
  arguments.append(
211
  PromptArgument(
212
  name=param_name,
213
+ description=arg_description,
214
  required=param_name in parameters.get("required", []),
215
  )
216
  )
217
 
 
 
 
218
  return cls(
219
  name=func_name,
220
  description=description,
 
224
  fn=fn,
225
  )
226
 
227
+ def _convert_string_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]:
228
+ """Convert string arguments to expected types based on function signature."""
229
+ from fastmcp.server.context import Context
230
+
231
+ sig = inspect.signature(self.fn)
232
+ converted_kwargs = {}
233
+
234
+ # Find context parameter name if any
235
+ context_param_name = find_kwarg_by_type(self.fn, kwarg_type=Context)
236
+
237
+ for param_name, param_value in kwargs.items():
238
+ if param_name in sig.parameters:
239
+ param = sig.parameters[param_name]
240
+
241
+ # Skip Context parameters - they're handled separately
242
+ if param_name == context_param_name:
243
+ converted_kwargs[param_name] = param_value
244
+ continue
245
+
246
+ # If parameter has no annotation or annotation is str, pass as-is
247
+ if (
248
+ param.annotation == inspect.Parameter.empty
249
+ or param.annotation is str
250
+ ):
251
+ converted_kwargs[param_name] = param_value
252
+ # If argument is not a string, pass as-is (already properly typed)
253
+ elif not isinstance(param_value, str):
254
+ converted_kwargs[param_name] = param_value
255
+ else:
256
+ # Try to convert string argument using type adapter
257
+ try:
258
+ adapter = get_cached_typeadapter(param.annotation)
259
+ # Try JSON parsing first for complex types
260
+ try:
261
+ converted_kwargs[param_name] = adapter.validate_json(
262
+ param_value
263
+ )
264
+ except (ValueError, TypeError, pydantic_core.ValidationError):
265
+ # Fallback to direct validation
266
+ converted_kwargs[param_name] = adapter.validate_python(
267
+ param_value
268
+ )
269
+ except (ValueError, TypeError, pydantic_core.ValidationError) as e:
270
+ # If conversion fails, provide informative error
271
+ raise PromptError(
272
+ f"Could not convert argument '{param_name}' with value '{param_value}' "
273
+ f"to expected type {param.annotation}. Error: {e}"
274
+ )
275
+ else:
276
+ # Parameter not in function signature, pass as-is
277
+ converted_kwargs[param_name] = param_value
278
+
279
+ return converted_kwargs
280
+
281
  async def render(
282
  self,
283
  arguments: dict[str, Any] | None = None,
 
300
  if context_kwarg and context_kwarg not in kwargs:
301
  kwargs[context_kwarg] = get_context()
302
 
303
+ # Convert string arguments to expected types when needed
304
+ kwargs = self._convert_string_arguments(kwargs)
305
+
306
  # Call function and check if result is a coroutine
307
  result = self.fn(**kwargs)
308
  if inspect.iscoroutine(result):
tests/prompts/test_prompt.py CHANGED
@@ -240,3 +240,245 @@ class TestRenderPrompt:
240
  ),
241
  )
242
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  ),
241
  )
242
  ]
243
+
244
+
245
+ class TestPromptTypeConversion:
246
+ async def test_list_of_integers_as_string_args(self):
247
+ """Test that prompts can handle complex types passed as strings from MCP spec."""
248
+
249
+ def sum_numbers(numbers: list[int]) -> str:
250
+ """Calculate the sum of a list of numbers."""
251
+ total = sum(numbers)
252
+ return f"The sum is: {total}"
253
+
254
+ prompt = Prompt.from_function(sum_numbers)
255
+
256
+ # MCP spec only allows string arguments, so this should work
257
+ # after we implement type conversion
258
+ result_from_string = await prompt.render(
259
+ arguments={"numbers": "[1, 2, 3, 4, 5]"}
260
+ )
261
+ assert result_from_string == [
262
+ PromptMessage(
263
+ role="user", content=TextContent(type="text", text="The sum is: 15")
264
+ )
265
+ ]
266
+
267
+ # Both should work now with string conversion
268
+ result_from_list_string = await prompt.render(
269
+ arguments={"numbers": "[1, 2, 3, 4, 5]"}
270
+ )
271
+ assert result_from_list_string == result_from_string
272
+
273
+ async def test_various_type_conversions(self):
274
+ """Test type conversion for various data types."""
275
+
276
+ def process_data(
277
+ name: str,
278
+ age: int,
279
+ scores: list[float],
280
+ metadata: dict[str, str],
281
+ active: bool,
282
+ ) -> str:
283
+ return f"{name} ({age}): {len(scores)} scores, active={active}, metadata keys={list(metadata.keys())}"
284
+
285
+ prompt = Prompt.from_function(process_data)
286
+
287
+ # All arguments as strings (as MCP would send them)
288
+ result = await prompt.render(
289
+ arguments={
290
+ "name": "Alice",
291
+ "age": "25",
292
+ "scores": "[1.5, 2.0, 3.5]",
293
+ "metadata": '{"project": "test", "version": "1.0"}',
294
+ "active": "true",
295
+ }
296
+ )
297
+
298
+ expected_text = (
299
+ "Alice (25): 3 scores, active=True, metadata keys=['project', 'version']"
300
+ )
301
+ assert result == [
302
+ PromptMessage(
303
+ role="user", content=TextContent(type="text", text=expected_text)
304
+ )
305
+ ]
306
+
307
+ async def test_type_conversion_error_handling(self):
308
+ """Test that informative errors are raised for invalid type conversions."""
309
+ from fastmcp.exceptions import PromptError
310
+
311
+ def typed_prompt(numbers: list[int]) -> str:
312
+ return f"Got {len(numbers)} numbers"
313
+
314
+ prompt = Prompt.from_function(typed_prompt)
315
+
316
+ # Test with invalid JSON - should raise PromptError due to exception handling in render()
317
+ with pytest.raises(PromptError) as exc_info:
318
+ await prompt.render(arguments={"numbers": "not valid json"})
319
+
320
+ assert f"Error rendering prompt {prompt.name}" in str(exc_info.value)
321
+
322
+ async def test_json_parsing_fallback(self):
323
+ """Test that JSON parsing falls back to direct validation when needed."""
324
+
325
+ def data_prompt(value: int) -> str:
326
+ return f"Value: {value}"
327
+
328
+ prompt = Prompt.from_function(data_prompt)
329
+
330
+ # This should work with JSON parsing (integer as string)
331
+ result1 = await prompt.render(arguments={"value": "42"})
332
+ assert result1 == [
333
+ PromptMessage(
334
+ role="user", content=TextContent(type="text", text="Value: 42")
335
+ )
336
+ ]
337
+
338
+ # This should work with direct validation (already an integer string)
339
+ result2 = await prompt.render(arguments={"value": "123"})
340
+ assert result2 == [
341
+ PromptMessage(
342
+ role="user", content=TextContent(type="text", text="Value: 123")
343
+ )
344
+ ]
345
+
346
+ async def test_mixed_string_and_typed_args(self):
347
+ """Test mixing string args (no conversion) with typed args (conversion needed)."""
348
+
349
+ def mixed_prompt(message: str, count: int) -> str:
350
+ return f"{message} (repeated {count} times)"
351
+
352
+ prompt = Prompt.from_function(mixed_prompt)
353
+
354
+ result = await prompt.render(
355
+ arguments={
356
+ "message": "Hello world", # str - no conversion needed
357
+ "count": "3", # int - conversion needed
358
+ }
359
+ )
360
+
361
+ assert result == [
362
+ PromptMessage(
363
+ role="user",
364
+ content=TextContent(type="text", text="Hello world (repeated 3 times)"),
365
+ )
366
+ ]
367
+
368
+
369
+ class TestPromptArgumentDescriptions:
370
+ def test_enhanced_descriptions_for_non_string_types(self):
371
+ """Test that non-string argument types get enhanced descriptions with JSON schema."""
372
+
373
+ def analyze_data(
374
+ name: str,
375
+ numbers: list[int],
376
+ metadata: dict[str, str],
377
+ threshold: float,
378
+ active: bool,
379
+ ) -> str:
380
+ """Analyze numerical data."""
381
+ return f"Analyzed {name}"
382
+
383
+ prompt = Prompt.from_function(analyze_data)
384
+
385
+ assert prompt.arguments is not None
386
+ # Check that string parameter has no schema enhancement
387
+ name_arg = next((arg for arg in prompt.arguments if arg.name == "name"), None)
388
+ assert name_arg is not None
389
+ assert name_arg.description is None # No enhancement for string types
390
+
391
+ # Check that non-string parameters have schema enhancements
392
+ numbers_arg = next(
393
+ (arg for arg in prompt.arguments if arg.name == "numbers"), None
394
+ )
395
+ assert numbers_arg is not None
396
+ assert numbers_arg.description is not None
397
+ assert (
398
+ "Provide as a JSON string matching the following schema:"
399
+ in numbers_arg.description
400
+ )
401
+ assert '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description
402
+
403
+ metadata_arg = next(
404
+ (arg for arg in prompt.arguments if arg.name == "metadata"), None
405
+ )
406
+ assert metadata_arg is not None
407
+ assert metadata_arg.description is not None
408
+ assert (
409
+ "Provide as a JSON string matching the following schema:"
410
+ in metadata_arg.description
411
+ )
412
+ assert (
413
+ '{"additionalProperties":{"type":"string"},"type":"object"}'
414
+ in metadata_arg.description
415
+ )
416
+
417
+ threshold_arg = next(
418
+ (arg for arg in prompt.arguments if arg.name == "threshold"), None
419
+ )
420
+ assert threshold_arg is not None
421
+ assert threshold_arg.description is not None
422
+ assert (
423
+ "Provide as a JSON string matching the following schema:"
424
+ in threshold_arg.description
425
+ )
426
+ assert '{"type":"number"}' in threshold_arg.description
427
+
428
+ active_arg = next(
429
+ (arg for arg in prompt.arguments if arg.name == "active"), None
430
+ )
431
+ assert active_arg is not None
432
+ assert active_arg.description is not None
433
+ assert (
434
+ "Provide as a JSON string matching the following schema:"
435
+ in active_arg.description
436
+ )
437
+ assert '{"type":"boolean"}' in active_arg.description
438
+
439
+ def test_enhanced_descriptions_with_existing_descriptions(self):
440
+ """Test that existing parameter descriptions are preserved with schema appended."""
441
+ from typing import Annotated
442
+
443
+ from pydantic import Field
444
+
445
+ def documented_prompt(
446
+ numbers: Annotated[
447
+ list[int], Field(description="A list of integers to process")
448
+ ],
449
+ ) -> str:
450
+ """Process numbers."""
451
+ return "processed"
452
+
453
+ prompt = Prompt.from_function(documented_prompt)
454
+
455
+ assert prompt.arguments is not None
456
+ numbers_arg = next(
457
+ (arg for arg in prompt.arguments if arg.name == "numbers"), None
458
+ )
459
+ assert numbers_arg is not None
460
+ # Should have both the original description and the schema
461
+ assert numbers_arg.description is not None
462
+ assert "A list of integers to process" in numbers_arg.description
463
+ assert "\n\n" in numbers_arg.description # Should have newline separator
464
+ assert (
465
+ "Provide as a JSON string matching the following schema:"
466
+ in numbers_arg.description
467
+ )
468
+
469
+ def test_string_parameters_no_enhancement(self):
470
+ """Test that string parameters don't get schema enhancement."""
471
+
472
+ def string_only_prompt(message: str, name: str) -> str:
473
+ return f"{message}, {name}"
474
+
475
+ prompt = Prompt.from_function(string_only_prompt)
476
+
477
+ assert prompt.arguments is not None
478
+ for arg in prompt.arguments:
479
+ # String parameters should not have schema enhancement
480
+ if arg.description is not None:
481
+ assert (
482
+ "Provide as a JSON string matching the following schema:"
483
+ not in arg.description
484
+ )
tests/server/test_server_interactions.py CHANGED
@@ -1785,6 +1785,62 @@ class TestPrompts:
1785
  assert prompts[0].arguments[1].name == "optional"
1786
  assert prompts[0].arguments[1].required is False
1787
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1788
  async def test_get_prompt(self):
1789
  """Test getting a prompt through MCP protocol."""
1790
  mcp = FastMCP()
 
1785
  assert prompts[0].arguments[1].name == "optional"
1786
  assert prompts[0].arguments[1].required is False
1787
 
1788
+ async def test_list_prompts_with_enhanced_descriptions(self):
1789
+ """Test that enhanced descriptions with JSON schema are visible via MCP protocol."""
1790
+ mcp = FastMCP()
1791
+
1792
+ @mcp.prompt
1793
+ def analyze_data(
1794
+ name: str, numbers: list[int], metadata: dict[str, str], threshold: float
1795
+ ) -> str:
1796
+ """Analyze some data."""
1797
+ return f"Analyzed {name}"
1798
+
1799
+ async with Client(mcp) as client:
1800
+ prompts = await client.list_prompts()
1801
+ assert len(prompts) == 1
1802
+ prompt = prompts[0]
1803
+ assert prompt.name == "analyze_data"
1804
+ assert prompt.description == "Analyze some data."
1805
+
1806
+ # Find each argument and verify schema enhancements
1807
+ assert prompt.arguments is not None
1808
+ args_by_name = {arg.name: arg for arg in prompt.arguments}
1809
+
1810
+ # String parameter should not have schema enhancement
1811
+ name_arg = args_by_name["name"]
1812
+ assert name_arg.description is None
1813
+
1814
+ # Non-string parameters should have schema enhancements
1815
+ numbers_arg = args_by_name["numbers"]
1816
+ assert numbers_arg.description is not None
1817
+ assert (
1818
+ "Provide as a JSON string matching the following schema:"
1819
+ in numbers_arg.description
1820
+ )
1821
+ assert (
1822
+ '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description
1823
+ )
1824
+
1825
+ metadata_arg = args_by_name["metadata"]
1826
+ assert metadata_arg.description is not None
1827
+ assert (
1828
+ "Provide as a JSON string matching the following schema:"
1829
+ in metadata_arg.description
1830
+ )
1831
+ assert (
1832
+ '{"additionalProperties":{"type":"string"},"type":"object"}'
1833
+ in metadata_arg.description
1834
+ )
1835
+
1836
+ threshold_arg = args_by_name["threshold"]
1837
+ assert threshold_arg.description is not None
1838
+ assert (
1839
+ "Provide as a JSON string matching the following schema:"
1840
+ in threshold_arg.description
1841
+ )
1842
+ assert '{"type":"number"}' in threshold_arg.description
1843
+
1844
  async def test_get_prompt(self):
1845
  """Test getting a prompt through MCP protocol."""
1846
  mcp = FastMCP()