-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Closed
Labels
wontfixThis will not be worked onThis will not be worked on
Description
Initial Checks
- I confirm that I'm using the latest version of MCP Python SDK
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Description
When I run the example code below there are couple of issues I see in MCP inspector regarding the tools/list call
Pasting the response here for clarity
{
"tools": [
{
"name": "sample_tool_with_params",
"description": "A tool with parameters",
"inputSchema": {
"type": "object",
"properties": {
"param1": {
"description": "Normal Required Param",
"title": "Param1",
"type": "string"
},
"param2": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Normal Param Defined as Optional without a default value in Pydantic Field",
"title": "Param2"
},
"param3": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Normal Param Defined as Optional with a default value in Pydantic Field",
"title": "Param3"
}
},
"required": [
"param1",
"param2"
],
"title": "sample_tool_with_paramsArguments"
}
}
]
}
Issues:
- If I mark a tool param as optional(like param2 in the example provided), but do not put default value as none in pydantic field, it still ads it to the required fields list indicating the SDK is using the default value to decide which parameters are required and optional instead of the parameter type
- For parameters which are marked as optional, the type is set as part of an "anyOf" array with type being either string or null in this case. This behaviour is different from expectation as per spec and also the typescript sdk where we rely only on the required array to decide the required params. For example param3 has both type set as Optional[str] and has default value set to none, so it does not come in the required list, but when making the actual tool call it forces the client to send the value of param3 as
null
instead of ommiting the param all together. Attaching tool\call request and response below:
Request:
{
"method": "tools/call",
"params": {
"name": "sample_tool_with_params",
"arguments": {
"param1": "abc",
"param2": "def",
"param3": null
},
"_meta": {
"progressToken": 1
}
}
}
Response:
{
"content": [
{
"type": "text",
"text": "{\n \"param1\": \"abc\",\n \"param2\": \"def\",\n \"param3\": null\n}"
}
],
"isError": false
}
I have verifed this behaviour with the typescript sdk specifically with the exaple server here
Here you can look at read_text_file
tool for how optional parameters are handled. Attaching an excerpt from tools/list to show how it behaves
{
"name": "read_text_file",
"description": "Read the complete contents of a file from the file system as text. Handles various text encodings and provides detailed error messages if the file cannot be read. Use this tool when you need to examine the contents of a single file. Use the 'head' parameter to read only the first N lines of a file, or the 'tail' parameter to read only the last N lines of a file. Operates on the file as text regardless of extension. Only works within allowed directories.",
"inputSchema": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
"tail": {
"type": "number",
"description": "If provided, returns only the last N lines of the file"
},
"head": {
"type": "number",
"description": "If provided, returns only the first N lines of the file"
}
},
"required": [
"path"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
}
Example Code
from mcp.server.fastmcp import FastMCP
from pydantic import Field
from typing import Optional
# Create MCP server instance
mcp = FastMCP()
@mcp.tool()
def sample_tool_with_params(
param1: str = Field(description="Normal Required Param"),
param2: Optional[str] = Field(description="Normal Param Defined as Optional without a default value in Pydantic Field"),
param3: Optional[str] = Field(default=None, description="Normal Param Defined as Optional with a default value in Pydantic Field")
) -> dict:
"A tool with parameters"
return {
"param1": param1,
"param2": param2,
"param3": param3
}
def main():
"""Run the MCP server"""
mcp.run(transport='streamable-http')
if __name__ == "__main__":
main()
Python & MCP Python SDK
Python: 3.13.5
MCP SDK: 1.14.1
Metadata
Metadata
Assignees
Labels
wontfixThis will not be worked onThis will not be worked on