diff --git a/src/mcp/types/__init__.py b/src/mcp/types/__init__.py index b44230393..47e8f56cc 100644 --- a/src/mcp/types/__init__.py +++ b/src/mcp/types/__init__.py @@ -59,6 +59,7 @@ ElicitResult, EmbeddedResource, EmptyResult, + FileInputDescriptor, FormElicitationCapability, GetPromptRequest, GetPromptRequestParams, @@ -303,6 +304,7 @@ "Task", "TaskMetadata", "RelatedTaskMetadata", + "FileInputDescriptor", "Tool", "ToolAnnotations", "ToolChoice", diff --git a/src/mcp/types/_types.py b/src/mcp/types/_types.py index 9005d253a..9cfaf37f2 100644 --- a/src/mcp/types/_types.py +++ b/src/mcp/types/_types.py @@ -1150,6 +1150,38 @@ class ToolExecution(MCPModel): """ +class FileInputDescriptor(MCPModel): + """Value of the ``mcpFile`` JSON Schema extension keyword. + + When present on a ``{"type": "string", "format": "uri"}`` property in a + :class:`Tool` ``input_schema`` or an elicitation ``requested_schema``, it + marks that property as a file input that clients SHOULD render as a native + file picker. Selected files are encoded as RFC 2397 data URIs. + + Both fields are advisory; servers MUST still validate inputs independently. + """ + + accept: list[str] | None = None + """Media type patterns and/or file extensions the client SHOULD filter the + picker to. + + Supports exact MIME types (``"image/png"``), wildcard subtypes + (``"image/*"``), and dot-prefixed extensions (``".pdf"``) following the same + grammar as the HTML ``accept`` attribute. Extension entries are picker hints + only; server-side validation compares MIME types. If omitted, any file type + is accepted. + """ + + max_size: int | None = None + """Maximum decoded file size in bytes that the server will accept inline as + a data URI. + + Servers MUST reject larger payloads with the ``"file_too_large"`` structured + error reason. For files larger than this, servers obtain the file via + URL-mode elicitation instead of this property. + """ + + class Tool(BaseMetadata): """Definition for a tool the client can call.""" diff --git a/tests/test_types.py b/tests/test_types.py index f424efdbf..ae4d0c93c 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -8,6 +8,8 @@ CreateMessageRequestParams, CreateMessageResult, CreateMessageResultWithTools, + ElicitRequestFormParams, + FileInputDescriptor, Implementation, InitializeRequest, InitializeRequestParams, @@ -360,3 +362,77 @@ def test_list_tools_result_preserves_json_schema_2020_12_fields(): assert tool.input_schema["$schema"] == "https://json-schema.org/draft/2020-12/schema" assert "$defs" in tool.input_schema assert tool.input_schema["additionalProperties"] is False + + +def test_file_input_descriptor_roundtrip(): + """FileInputDescriptor serializes maxSize camelCase and accepts MIME + extension patterns.""" + wire: dict[str, Any] = {"accept": ["image/png", "image/*", ".pdf"], "maxSize": 1048576} + desc = FileInputDescriptor.model_validate(wire) + assert desc.accept == ["image/png", "image/*", ".pdf"] + assert desc.max_size == 1048576 + + dumped = desc.model_dump(by_alias=True, exclude_none=True) + assert dumped == {"accept": ["image/png", "image/*", ".pdf"], "maxSize": 1048576} + + # Both fields are optional; empty descriptor is valid + empty = FileInputDescriptor.model_validate({}) + assert empty.accept is None + assert empty.max_size is None + assert empty.model_dump(by_alias=True, exclude_none=True) == {} + + +def test_tool_input_schema_mcpfile_roundtrip(): + """mcpFile keyword in Tool.input_schema survives roundtrip and parses as FileInputDescriptor.""" + wire: dict[str, Any] = { + "name": "describe_image", + "description": "Describe the contents of an image.", + "inputSchema": { + "type": "object", + "properties": { + "image": { + "type": "string", + "format": "uri", + "mcpFile": {"accept": ["image/png", "image/jpeg"], "maxSize": 5242880}, + }, + }, + "required": ["image"], + }, + } + tool = Tool.model_validate(wire) + image_prop = tool.input_schema["properties"]["image"] + assert image_prop["mcpFile"] == {"accept": ["image/png", "image/jpeg"], "maxSize": 5242880} + + desc = FileInputDescriptor.model_validate(image_prop["mcpFile"]) + assert desc.accept == ["image/png", "image/jpeg"] + assert desc.max_size == 5242880 + + dumped = tool.model_dump(by_alias=True, exclude_none=True) + assert dumped["inputSchema"]["properties"]["image"]["mcpFile"]["maxSize"] == 5242880 + + +def test_elicit_form_params_mcpfile_roundtrip(): + """mcpFile keyword in requested_schema survives roundtrip and parses as FileInputDescriptor.""" + wire: dict[str, Any] = { + "mode": "form", + "message": "Please select a profile photo.", + "requestedSchema": { + "type": "object", + "properties": { + "photo": { + "type": "string", + "format": "uri", + "title": "Profile photo", + "mcpFile": {"accept": ["image/*"], "maxSize": 2097152}, + }, + }, + "required": ["photo"], + }, + } + params = ElicitRequestFormParams.model_validate(wire) + photo_prop = params.requested_schema["properties"]["photo"] + desc = FileInputDescriptor.model_validate(photo_prop["mcpFile"]) + assert desc.accept == ["image/*"] + assert desc.max_size == 2097152 + + dumped = params.model_dump(by_alias=True, exclude_none=True) + assert dumped["requestedSchema"]["properties"]["photo"]["mcpFile"]["maxSize"] == 2097152