Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prompt argument types should be passed to the client in prompts/list request to enable client side validation of inputs #153

Open
wanderingnature opened this issue Jan 16, 2025 · 1 comment

Comments

@wanderingnature
Copy link

Is your feature request related to a problem? Please describe.
An MCP server will expose the types of prompt arguments to the client if you add 'type=' to the PromptArgument. i.e.

PromptArgument(
                    name=CodePromptArgs.NOTES,
                    description="Notes about the task",
                    type="str",
                    required=False,
                ),

The client receives this information and then could generate a correct pydantic model to validate input to the prompt before sending to the server.
{'name': 'notes', 'description': 'Notes about the task', 'required': False, 'type': 'str'},

This is not possible in FastMCP.

Describe the solution you'd like
types of prompt arguments should be sent to the client in FastMCP the same as in MCP. Annotating the fields with type input still does not send the type. As a work-around you can put the type into the description field but that requires the client to parse the field to get the type which is not as reliable as sending the annotated type directly as you can in MCP (although that's not documented...)

It isn't clear how to construct a PromptArgument in FastMCP, and it seems to not be possible.
I have tried to create a PromptArgument and add it to a prompt, but the server immediately fails with:
[01/16/25 12:40:19] ERROR Failed to run server: name cli.py:329

prompt_arg_name = PromptArgument(
    name=MessageTaskInput.name,
    description="name of the user",
    type="str",
    required=True,
)


@mcp.prompt("updates")
def updates_prompt(
    prompt_arg_name
) -> GetPromptResult:

obviously that's not correct - but if you could construct and use a PromptArgument it seems you would get the same behavior as in MCP.

Describe alternatives you've considered
I tried a hot fix that added 'type' to PromptArguments in both base.py and types.py. This does get the type to the client but it is always 'None'. I cannot figure out how MCP sends the type, since the standard model class for PromptArgument does not include 'type' as a field, although there is model_config = ConfigDict(extra="allow") which is probably why it can use 'type'.

Additional context
You can see for an MCP server the client receives all the data about the arguments for the prompt:
arguments
[PromptArgument(name='task_description', description='Description of the code task to perform', required=True, type='str'), PromptArgument(name='notes', description='Notes about the task', required=False, type='str'), PromptArgument(name='python_file_list', description='Content of Python files to analyze/modify', required=False, type='str'), PromptArgument(name='allowed_path', description='Allowed path for writing Markdown files', required=True, type='str'), PromptArgument(name='project_path', description='Project code sources path', required=True, type='str')]

For a FastMCP server the client does not receive the type at all unless the mentioned hot-fix is applied, and then it is always 'None'.

Arguments: [PromptArgument(name='name', description='Name of the user (str)', required=True, type=None), PromptArgument(name='number', description='Lucky number (int)', required=True, type=None), PromptArgument(name='email', description='Contact email address (EmailStr|str)', required=True, type=None), PromptArgument(name='subscribe', description='Whether to subscribe to updates (bool)', required=True, type=None)]

You can see in that output that I have a work-around which is to document the type within the annotation of the prompt function arguments.

Arguments: [PromptArgument(name='name', description='Name of the user (str)', required=True, type=None), PromptArgument(name='number', description='Lucky number (int)', required=True, type=None), PromptArgument(name='email', description='Contact email address (EmailStr|str)', required=True, type=None), PromptArgument(name='subscribe', description='Whether to subscribe to updates (bool)', required=True, type=None)]

@mcp.prompt("updates")
def updates_prompt(
name: Annotated[str, Field(description="Name of the user (str)")],
number: Annotated[int, Field(description="Lucky number (int)")],
email: Annotated[EmailStr, Field(description="Contact email address (EmailStr|str)")],
subscribe: Annotated[bool, Field(description="Whether to subscribe to updates (bool)")]
) -> GetPromptResult:

Again note 'type=None' is present because I applied the hot-fix (added type to PromptArguments class in base.py and types.py) but the type is not passed through. I also added this into base.py at @classmethod def from_function but type is still 'None' at the client.

                arguments.append(
                    PromptArgument(
                        name=param_name,
                        description=param.get("description"),
                        required=required,
                        type=param.get("type") # hotfix
                    )
                )
@wanderingnature
Copy link
Author

wanderingnature commented Jan 16, 2025

Poked around sources some more and found in mcp/server/fastmcp/server.py the rest of the fix - in addition to what is needed above.

CURRENT (no type=)

    async def list_prompts(self) -> list[MCPPrompt]:
        """List all available prompts."""
        prompts = self._prompt_manager.list_prompts()
        return [
            MCPPrompt(
                name=prompt.name,
                description=prompt.description,
                arguments=[
                    MCPPromptArgument(
                        name=arg.name,
                        description=arg.description,
                        required=arg.required
                    )
                    for arg in (prompt.arguments or [])
                ],
            )
            for prompt in prompts
        ]

Argument Result: ('type': None)

Argument: name
Full argument data: {'name': 'name', 'description': 'Name of the user (str)', 'required': True, 'type': None}
Model fields: {'name': FieldInfo(annotation=str, required=True), 'description': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'required': FieldInfo(annotation=Union[bool, NoneType], required=False, default=None), 'type': FieldInfo(annotation=Union[str, NoneType], required=False, default=None)}

CHANGED (added type=)

    async def list_prompts(self) -> list[MCPPrompt]:
        """List all available prompts."""
        prompts = self._prompt_manager.list_prompts()
        return [
            MCPPrompt(
                name=prompt.name,
                description=prompt.description,
                arguments=[
                    MCPPromptArgument(
                        name=arg.name,
                        description=arg.description,
                        required=arg.required,
                        type=arg.type.  # add to get type on client
                    )
                    for arg in (prompt.arguments or [])
                ],
            )
            for prompt in prompts
        ]

Argument Result ('type': string)

Argument: name
Full argument data: {'name': 'name', 'description': 'Name of the user (str)', 'required': True, 'type': 'string'}
Model fields: {'name': FieldInfo(annotation=str, required=True), 'description': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'required': FieldInfo(annotation=Union[bool, NoneType], required=False, default=None), 'type': FieldInfo(annotation=Union[str, NoneType], required=False, default=None)}

I'll open a PR by Monday if no one else can get to this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant