Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/mcp/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
ElicitResult,
EmbeddedResource,
EmptyResult,
FileInputDescriptor,
FormElicitationCapability,
GetPromptRequest,
GetPromptRequestParams,
Expand Down Expand Up @@ -303,6 +304,7 @@
"Task",
"TaskMetadata",
"RelatedTaskMetadata",
"FileInputDescriptor",
"Tool",
"ToolAnnotations",
"ToolChoice",
Expand Down
32 changes: 32 additions & 0 deletions src/mcp/types/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
76 changes: 76 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
CreateMessageRequestParams,
CreateMessageResult,
CreateMessageResultWithTools,
ElicitRequestFormParams,
FileInputDescriptor,
Implementation,
InitializeRequest,
InitializeRequestParams,
Expand Down Expand Up @@ -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
Loading