Jeremiah Lowin Claude commited on
Commit
65ead06
·
1 Parent(s): b07d252

Add automatic JSON schema descriptions for non-string prompt arguments

Browse files

- Fix ValueError -> PromptError for consistent error handling
- Add automatic JSON schema descriptions to non-string prompt arguments
- Include comprehensive tests for argument description enhancement
- Verify enhanced descriptions are visible via MCP protocol

This helps developers understand the expected string format for complex
types when calling prompts from MCP clients.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

src/fastmcp/prompts/prompt.py CHANGED
@@ -3,6 +3,7 @@
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 Any
@@ -177,10 +178,39 @@ class FunctionPrompt(Prompt):
177
  arguments: list[PromptArgument] = []
178
  if "properties" in parameters:
179
  for param_name, param in parameters["properties"].items():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  arguments.append(
181
  PromptArgument(
182
  name=param_name,
183
- description=param.get("description"),
184
  required=param_name in parameters.get("required", []),
185
  )
186
  )
@@ -238,7 +268,7 @@ class FunctionPrompt(Prompt):
238
  )
239
  except (ValueError, TypeError, pydantic_core.ValidationError) as e:
240
  # If conversion fails, provide informative error
241
- raise ValueError(
242
  f"Could not convert argument '{param_name}' with value '{param_value}' "
243
  f"to expected type {param.annotation}. Error: {e}"
244
  )
 
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
 
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"Arguments must be strings conforming to this JSON 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
  )
 
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
  )
tests/prompts/test_prompt.py CHANGED
@@ -364,3 +364,98 @@ class TestPromptTypeConversion:
364
  content=TextContent(type="text", text="Hello world (repeated 3 times)"),
365
  )
366
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Check that string parameter has no schema enhancement
386
+ name_arg = next(arg for arg in prompt.arguments if arg.name == "name")
387
+ assert name_arg.description is None # No enhancement for string types
388
+
389
+ # Check that non-string parameters have schema enhancements
390
+ numbers_arg = next(arg for arg in prompt.arguments if arg.name == "numbers")
391
+ assert (
392
+ "Arguments must be strings conforming to this JSON schema:"
393
+ in numbers_arg.description
394
+ )
395
+ assert '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description
396
+
397
+ metadata_arg = next(arg for arg in prompt.arguments if arg.name == "metadata")
398
+ assert (
399
+ "Arguments must be strings conforming to this JSON schema:"
400
+ in metadata_arg.description
401
+ )
402
+ assert (
403
+ '{"additionalProperties":{"type":"string"},"type":"object"}'
404
+ in metadata_arg.description
405
+ )
406
+
407
+ threshold_arg = next(arg for arg in prompt.arguments if arg.name == "threshold")
408
+ assert (
409
+ "Arguments must be strings conforming to this JSON schema:"
410
+ in threshold_arg.description
411
+ )
412
+ assert '{"type":"number"}' in threshold_arg.description
413
+
414
+ active_arg = next(arg for arg in prompt.arguments if arg.name == "active")
415
+ assert (
416
+ "Arguments must be strings conforming to this JSON schema:"
417
+ in active_arg.description
418
+ )
419
+ assert '{"type":"boolean"}' in active_arg.description
420
+
421
+ def test_enhanced_descriptions_with_existing_descriptions(self):
422
+ """Test that existing parameter descriptions are preserved with schema appended."""
423
+ from typing import Annotated
424
+
425
+ from pydantic import Field
426
+
427
+ def documented_prompt(
428
+ numbers: Annotated[
429
+ list[int], Field(description="A list of integers to process")
430
+ ],
431
+ ) -> str:
432
+ """Process numbers."""
433
+ return "processed"
434
+
435
+ prompt = Prompt.from_function(documented_prompt)
436
+
437
+ numbers_arg = next(arg for arg in prompt.arguments if arg.name == "numbers")
438
+ # Should have both the original description and the schema
439
+ assert numbers_arg.description is not None
440
+ assert "A list of integers to process" in numbers_arg.description
441
+ assert "\n\n" in numbers_arg.description # Should have newline separator
442
+ assert (
443
+ "Arguments must be strings conforming to this JSON schema:"
444
+ in numbers_arg.description
445
+ )
446
+
447
+ def test_string_parameters_no_enhancement(self):
448
+ """Test that string parameters don't get schema enhancement."""
449
+
450
+ def string_only_prompt(message: str, name: str) -> str:
451
+ return f"{message}, {name}"
452
+
453
+ prompt = Prompt.from_function(string_only_prompt)
454
+
455
+ for arg in prompt.arguments:
456
+ # String parameters should not have schema enhancement
457
+ if arg.description:
458
+ assert (
459
+ "Arguments must be strings conforming to this JSON schema:"
460
+ not in arg.description
461
+ )
tests/server/test_server_interactions.py CHANGED
@@ -1785,6 +1785,58 @@ 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
+ args_by_name = {arg.name: arg for arg in prompt.arguments}
1808
+
1809
+ # String parameter should not have schema enhancement
1810
+ name_arg = args_by_name["name"]
1811
+ assert name_arg.description is None
1812
+
1813
+ # Non-string parameters should have schema enhancements
1814
+ numbers_arg = args_by_name["numbers"]
1815
+ assert (
1816
+ "Arguments must be strings conforming to this JSON schema:"
1817
+ in numbers_arg.description
1818
+ )
1819
+ assert (
1820
+ '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description
1821
+ )
1822
+
1823
+ metadata_arg = args_by_name["metadata"]
1824
+ assert (
1825
+ "Arguments must be strings conforming to this JSON schema:"
1826
+ in metadata_arg.description
1827
+ )
1828
+ assert (
1829
+ '{"additionalProperties":{"type":"string"},"type":"object"}'
1830
+ in metadata_arg.description
1831
+ )
1832
+
1833
+ threshold_arg = args_by_name["threshold"]
1834
+ assert (
1835
+ "Arguments must be strings conforming to this JSON schema:"
1836
+ in threshold_arg.description
1837
+ )
1838
+ assert '{"type":"number"}' in threshold_arg.description
1839
+
1840
  async def test_get_prompt(self):
1841
  """Test getting a prompt through MCP protocol."""
1842
  mcp = FastMCP()