Add automatic JSON schema descriptions for non-string prompt arguments

- 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>
This commit is contained in:
Jeremiah Lowin 2025-06-22 09:38:57 -04:00
commit 817018bf3b
3 changed files with 179 additions and 2 deletions

View file

@ -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}"
)

View file

@ -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
)

View file

@ -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()