From 817018bf3b45a5f12f57acd3d8ed96f565dfe5dd Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Sun, 22 Jun 2025 09:38:57 -0400 Subject: [PATCH] Add automatic JSON schema descriptions for non-string prompt arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/fastmcp/prompts/prompt.py | 34 ++++++++- tests/prompts/test_prompt.py | 95 ++++++++++++++++++++++++ tests/server/test_server_interactions.py | 52 +++++++++++++ 3 files changed, 179 insertions(+), 2 deletions(-) diff --git a/src/fastmcp/prompts/prompt.py b/src/fastmcp/prompts/prompt.py index 37cf7c720..b88001662 100644 --- a/src/fastmcp/prompts/prompt.py +++ b/src/fastmcp/prompts/prompt.py @@ -3,6 +3,7 @@ from __future__ import annotations as _annotations import inspect +import json from abc import ABC, abstractmethod from collections.abc import Awaitable, Callable, Sequence from typing import Any @@ -177,10 +178,39 @@ class FunctionPrompt(Prompt): arguments: list[PromptArgument] = [] if "properties" in parameters: for param_name, param in parameters["properties"].items(): + arg_description = param.get("description") + + # For non-string parameters, append JSON schema info to help users + # understand the expected format when passing as strings (MCP requirement) + if param_name in sig.parameters: + sig_param = sig.parameters[param_name] + if ( + sig_param.annotation != inspect.Parameter.empty + and sig_param.annotation is not str + and param_name != context_kwarg + ): + # Get the JSON schema for this specific parameter type + try: + param_adapter = get_cached_typeadapter(sig_param.annotation) + param_schema = param_adapter.json_schema() + + # Create compact schema representation + schema_str = json.dumps(param_schema, separators=(",", ":")) + + # Append schema info to description + schema_note = f"Arguments must be strings conforming to this JSON schema: {schema_str}" + if arg_description: + arg_description = f"{arg_description}\n\n{schema_note}" + else: + arg_description = schema_note + except Exception: + # If schema generation fails, skip enhancement + pass + arguments.append( PromptArgument( name=param_name, - description=param.get("description"), + description=arg_description, required=param_name in parameters.get("required", []), ) ) @@ -238,7 +268,7 @@ class FunctionPrompt(Prompt): ) except (ValueError, TypeError, pydantic_core.ValidationError) as e: # If conversion fails, provide informative error - raise ValueError( + raise PromptError( f"Could not convert argument '{param_name}' with value '{param_value}' " f"to expected type {param.annotation}. Error: {e}" ) diff --git a/tests/prompts/test_prompt.py b/tests/prompts/test_prompt.py index 2283c224d..47d241ae9 100644 --- a/tests/prompts/test_prompt.py +++ b/tests/prompts/test_prompt.py @@ -364,3 +364,98 @@ class TestPromptTypeConversion: content=TextContent(type="text", text="Hello world (repeated 3 times)"), ) ] + + +class TestPromptArgumentDescriptions: + def test_enhanced_descriptions_for_non_string_types(self): + """Test that non-string argument types get enhanced descriptions with JSON schema.""" + + def analyze_data( + name: str, + numbers: list[int], + metadata: dict[str, str], + threshold: float, + active: bool, + ) -> str: + """Analyze numerical data.""" + return f"Analyzed {name}" + + prompt = Prompt.from_function(analyze_data) + + # Check that string parameter has no schema enhancement + name_arg = next(arg for arg in prompt.arguments if arg.name == "name") + assert name_arg.description is None # No enhancement for string types + + # Check that non-string parameters have schema enhancements + numbers_arg = next(arg for arg in prompt.arguments if arg.name == "numbers") + assert ( + "Arguments must be strings conforming to this JSON schema:" + in numbers_arg.description + ) + assert '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description + + metadata_arg = next(arg for arg in prompt.arguments if arg.name == "metadata") + assert ( + "Arguments must be strings conforming to this JSON schema:" + in metadata_arg.description + ) + assert ( + '{"additionalProperties":{"type":"string"},"type":"object"}' + in metadata_arg.description + ) + + threshold_arg = next(arg for arg in prompt.arguments if arg.name == "threshold") + assert ( + "Arguments must be strings conforming to this JSON schema:" + in threshold_arg.description + ) + assert '{"type":"number"}' in threshold_arg.description + + active_arg = next(arg for arg in prompt.arguments if arg.name == "active") + assert ( + "Arguments must be strings conforming to this JSON schema:" + in active_arg.description + ) + assert '{"type":"boolean"}' in active_arg.description + + def test_enhanced_descriptions_with_existing_descriptions(self): + """Test that existing parameter descriptions are preserved with schema appended.""" + from typing import Annotated + + from pydantic import Field + + def documented_prompt( + numbers: Annotated[ + list[int], Field(description="A list of integers to process") + ], + ) -> str: + """Process numbers.""" + return "processed" + + prompt = Prompt.from_function(documented_prompt) + + numbers_arg = next(arg for arg in prompt.arguments if arg.name == "numbers") + # Should have both the original description and the schema + assert numbers_arg.description is not None + assert "A list of integers to process" in numbers_arg.description + assert "\n\n" in numbers_arg.description # Should have newline separator + assert ( + "Arguments must be strings conforming to this JSON schema:" + in numbers_arg.description + ) + + def test_string_parameters_no_enhancement(self): + """Test that string parameters don't get schema enhancement.""" + + def string_only_prompt(message: str, name: str) -> str: + return f"{message}, {name}" + + prompt = Prompt.from_function(string_only_prompt) + + for arg in prompt.arguments: + # String parameters should not have schema enhancement + if arg.description: + assert ( + "Arguments must be strings conforming to this JSON schema:" + not in arg.description + ) diff --git a/tests/server/test_server_interactions.py b/tests/server/test_server_interactions.py index 6198a9f70..485621a7d 100644 --- a/tests/server/test_server_interactions.py +++ b/tests/server/test_server_interactions.py @@ -1785,6 +1785,58 @@ class TestPrompts: assert prompts[0].arguments[1].name == "optional" assert prompts[0].arguments[1].required is False + async def test_list_prompts_with_enhanced_descriptions(self): + """Test that enhanced descriptions with JSON schema are visible via MCP protocol.""" + mcp = FastMCP() + + @mcp.prompt + def analyze_data( + name: str, numbers: list[int], metadata: dict[str, str], threshold: float + ) -> str: + """Analyze some data.""" + return f"Analyzed {name}" + + async with Client(mcp) as client: + prompts = await client.list_prompts() + assert len(prompts) == 1 + prompt = prompts[0] + assert prompt.name == "analyze_data" + assert prompt.description == "Analyze some data." + + # Find each argument and verify schema enhancements + args_by_name = {arg.name: arg for arg in prompt.arguments} + + # String parameter should not have schema enhancement + name_arg = args_by_name["name"] + assert name_arg.description is None + + # Non-string parameters should have schema enhancements + numbers_arg = args_by_name["numbers"] + assert ( + "Arguments must be strings conforming to this JSON schema:" + in numbers_arg.description + ) + assert ( + '{"items":{"type":"integer"},"type":"array"}' in numbers_arg.description + ) + + metadata_arg = args_by_name["metadata"] + assert ( + "Arguments must be strings conforming to this JSON schema:" + in metadata_arg.description + ) + assert ( + '{"additionalProperties":{"type":"string"},"type":"object"}' + in metadata_arg.description + ) + + threshold_arg = args_by_name["threshold"] + assert ( + "Arguments must be strings conforming to this JSON schema:" + in threshold_arg.description + ) + assert '{"type":"number"}' in threshold_arg.description + async def test_get_prompt(self): """Test getting a prompt through MCP protocol.""" mcp = FastMCP()