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
4 changes: 4 additions & 0 deletions src/mcp/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
ElicitResult,
EmbeddedResource,
EmptyResult,
FileInputDescriptor,
FileInputsCapability,
FormElicitationCapability,
GetPromptRequest,
GetPromptRequestParams,
Expand Down Expand Up @@ -249,6 +251,7 @@
"ClientTasksRequestsCapability",
"CompletionsCapability",
"ElicitationCapability",
"FileInputsCapability",
"FormElicitationCapability",
"LoggingCapability",
"PromptsCapability",
Expand Down Expand Up @@ -303,6 +306,7 @@
"Task",
"TaskMetadata",
"RelatedTaskMetadata",
"FileInputDescriptor",
"Tool",
"ToolAnnotations",
"ToolChoice",
Expand Down
62 changes: 62 additions & 0 deletions src/mcp/types/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,16 @@ class SamplingToolsCapability(MCPModel):
"""


class FileInputsCapability(MCPModel):
"""Capability for declarative file inputs on tools and elicitation forms.

When a client declares this capability, servers may include ``input_files``
on Tool definitions and ``requested_files`` on form-mode elicitation
requests. Servers must not send those fields unless this capability is
present.
"""


class FormElicitationCapability(MCPModel):
"""Capability for form mode elicitation."""

Expand Down Expand Up @@ -323,6 +333,8 @@ class ClientCapabilities(MCPModel):
"""Present if the client supports listing roots."""
tasks: ClientTasksCapability | None = None
"""Present if the client supports task-augmented requests."""
file_inputs: FileInputsCapability | None = None
"""Present if the client supports declarative file inputs for tools and elicitation."""


class PromptsCapability(MCPModel):
Expand Down Expand Up @@ -1150,6 +1162,28 @@ class ToolExecution(MCPModel):
"""


class FileInputDescriptor(MCPModel):
"""Describes a single file input argument for a tool or elicitation form.

Provides optional hints for client-side file picker filtering and validation.
All fields are advisory; servers must still validate inputs independently.
"""

accept: list[str] | None = None
"""MIME type patterns the server will accept for this input.

Supports exact types (``"image/png"``) and wildcard subtypes (``"image/*"``).
If omitted, any file type is accepted.
"""

max_size: int | None = None
"""Maximum file size in bytes (decoded size, per file).

Servers should reject larger files with JSON-RPC ``-32602`` (Invalid Params)
and the structured reason ``"file_too_large"``.
"""


class Tool(BaseMetadata):
"""Definition for a tool the client can call."""

Expand All @@ -1174,6 +1208,20 @@ class Tool(BaseMetadata):

execution: ToolExecution | None = None

input_files: dict[str, FileInputDescriptor] | None = None
"""Declares which arguments in ``input_schema`` are file inputs.

Keys must match property names in ``input_schema["properties"]`` and the
corresponding schema properties must be ``{"type": "string", "format": "uri"}``
or an array thereof. Servers must not include this field unless the client
declared the ``file_inputs`` capability during initialization.

Clients should render a native file picker for these arguments and encode
selected files as RFC 2397 data URIs of the form
``data:<mediatype>;name=<filename>;base64,<data>`` where the ``name=``
parameter (percent-encoded) carries the original filename.
"""


class ListToolsResult(PaginatedResult):
"""The server's response to a tools/list request from the client."""
Expand Down Expand Up @@ -1649,6 +1697,20 @@ class ElicitRequestFormParams(RequestParams):
Only top-level properties are allowed, without nesting.
"""

requested_files: dict[str, FileInputDescriptor] | None = None
"""Declares which fields in ``requested_schema`` are file inputs.

Keys must match property names in ``requested_schema["properties"]`` and the
corresponding schema properties must be a string schema with ``format: "uri"``
or an array of such string schemas. Servers must not include this field unless
the client declared the ``file_inputs`` capability during initialization.

Clients should render a native file picker for these fields and encode
selected files as RFC 2397 data URIs of the form
``data:<mediatype>;name=<filename>;base64,<data>`` where the ``name=``
parameter (percent-encoded) carries the original filename.
"""


class ElicitRequestURLParams(RequestParams):
"""Parameters for URL mode elicitation requests.
Expand Down
108 changes: 108 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
CreateMessageRequestParams,
CreateMessageResult,
CreateMessageResultWithTools,
ElicitRequestFormParams,
FileInputDescriptor,
FileInputsCapability,
Implementation,
InitializeRequest,
InitializeRequestParams,
Expand Down Expand Up @@ -360,3 +363,108 @@ 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 patterns."""
wire: dict[str, Any] = {"accept": ["image/png", "image/*"], "maxSize": 1048576}
desc = FileInputDescriptor.model_validate(wire)
assert desc.accept == ["image/png", "image/*"]
assert desc.max_size == 1048576

dumped = desc.model_dump(by_alias=True, exclude_none=True)
assert dumped == {"accept": ["image/png", "image/*"], "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_with_input_files():
"""Tool.inputFiles round-trips via wire-format camelCase alias."""
wire: dict[str, Any] = {
"name": "upload_attachment",
"description": "Upload a file",
"inputSchema": {
"type": "object",
"properties": {
"file": {"type": "string", "format": "uri"},
"note": {"type": "string"},
},
"required": ["file"],
},
"inputFiles": {
"file": {"accept": ["application/pdf", "image/*"], "maxSize": 5242880},
},
}
tool = Tool.model_validate(wire)
assert tool.input_files is not None
assert set(tool.input_files.keys()) == {"file"}
assert isinstance(tool.input_files["file"], FileInputDescriptor)
assert tool.input_files["file"].accept == ["application/pdf", "image/*"]
assert tool.input_files["file"].max_size == 5242880

dumped = tool.model_dump(by_alias=True, exclude_none=True)
assert "inputFiles" in dumped
assert "input_files" not in dumped
assert dumped["inputFiles"]["file"]["maxSize"] == 5242880
assert dumped["inputFiles"]["file"]["accept"] == ["application/pdf", "image/*"]

# input_files defaults to None and is omitted when absent
plain = Tool(name="echo", input_schema={"type": "object"})
assert plain.input_files is None
assert "inputFiles" not in plain.model_dump(by_alias=True, exclude_none=True)


def test_client_capabilities_with_file_inputs():
"""ClientCapabilities.fileInputs round-trips as an empty capability object."""
caps = ClientCapabilities.model_validate({"fileInputs": {}})
assert caps.file_inputs is not None
assert isinstance(caps.file_inputs, FileInputsCapability)

dumped = caps.model_dump(by_alias=True, exclude_none=True)
assert dumped == {"fileInputs": {}}

# Absent by default
bare = ClientCapabilities.model_validate({})
assert bare.file_inputs is None
assert "fileInputs" not in bare.model_dump(by_alias=True, exclude_none=True)


def test_elicit_form_params_with_requested_files():
"""ElicitRequestFormParams.requestedFiles round-trips through the wire format."""
wire: dict[str, Any] = {
"mode": "form",
"message": "Upload your documents",
"requestedSchema": {
"type": "object",
"properties": {
"resume": {"type": "string", "format": "uri"},
"samples": {
"type": "array",
"items": {"type": "string", "format": "uri"},
"maxItems": 3,
},
},
"required": ["resume"],
},
"requestedFiles": {
"resume": {"accept": ["application/pdf"], "maxSize": 2097152},
"samples": {"accept": ["image/*"]},
},
}
params = ElicitRequestFormParams.model_validate(wire)
assert params.requested_files is not None
assert isinstance(params.requested_files["resume"], FileInputDescriptor)
assert params.requested_files["resume"].max_size == 2097152
assert params.requested_files["samples"].accept == ["image/*"]
assert params.requested_files["samples"].max_size is None

dumped = params.model_dump(by_alias=True, exclude_none=True)
assert "requestedFiles" in dumped
assert "requested_files" not in dumped
assert dumped["requestedFiles"]["resume"]["maxSize"] == 2097152
# samples had no maxSize; ensure it's excluded, not serialized as null
assert "maxSize" not in dumped["requestedFiles"]["samples"]
Loading