Skip to content

SDK does not handle optional parameters in tools properly #1402

@mukuagar

Description

@mukuagar

Initial Checks

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

No one assigned

    Labels

    wontfixThis will not be worked on

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions