mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 21:44:18 +02:00
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:
parent
ef1368fda5
commit
817018bf3b
3 changed files with 179 additions and 2 deletions
|
|
@ -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}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue