Skip to content

Commit 75374a0

Browse files
committed
chore(sep-2356): sync file-input types to spec PR @ef7d52f
The spec was substantially redesigned since the original SDK draft: the v1 sidecar-map model (Tool.inputFiles, ElicitRequestFormParams.requestedFiles, fileInputs client capability, StringArraySchema) was dropped in favor of an inline `mcpFile` JSON Schema extension keyword on string-uri properties. Since this SDK keeps both Tool.input_schema and ElicitRequestedSchema as dict[str, Any], the mcpFile keyword passes through untyped. The only typed surface is FileInputDescriptor itself, which servers can model_dump() into their schema dicts and clients can model_validate() out of them. Intentionally NOT adding a "file_too_large" error-reason constant: the structuredContent.reason vs outputSchema-conformance thread (pja-ant) on the spec PR is unresolved. - Remove FileInputsCapability + ClientCapabilities.file_inputs - Remove Tool.input_files - Remove ElicitRequestFormParams.requested_files - Update FileInputDescriptor docstrings to match spec (extension entries in accept, MUST-reject wording for maxSize, mcpFile keyword framing) - Tests now mirror the spec examples (mcpFile inline in inputSchema / requestedSchema, parsed via FileInputDescriptor.model_validate)
1 parent d12cf17 commit 75374a0

3 files changed

Lines changed: 54 additions & 118 deletions

File tree

src/mcp/types/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
EmbeddedResource,
6161
EmptyResult,
6262
FileInputDescriptor,
63-
FileInputsCapability,
6463
FormElicitationCapability,
6564
GetPromptRequest,
6665
GetPromptRequestParams,
@@ -251,7 +250,6 @@
251250
"ClientTasksRequestsCapability",
252251
"CompletionsCapability",
253252
"ElicitationCapability",
254-
"FileInputsCapability",
255253
"FormElicitationCapability",
256254
"LoggingCapability",
257255
"PromptsCapability",

src/mcp/types/_types.py

Lines changed: 19 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -222,16 +222,6 @@ class SamplingToolsCapability(MCPModel):
222222
"""
223223

224224

225-
class FileInputsCapability(MCPModel):
226-
"""Capability for declarative file inputs on tools and elicitation forms.
227-
228-
When a client declares this capability, servers may include ``input_files``
229-
on Tool definitions and ``requested_files`` on form-mode elicitation
230-
requests. Servers must not send those fields unless this capability is
231-
present.
232-
"""
233-
234-
235225
class FormElicitationCapability(MCPModel):
236226
"""Capability for form mode elicitation."""
237227

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

339327

340328
class PromptsCapability(MCPModel):
@@ -1163,24 +1151,34 @@ class ToolExecution(MCPModel):
11631151

11641152

11651153
class FileInputDescriptor(MCPModel):
1166-
"""Describes a single file input argument for a tool or elicitation form.
1154+
"""Value of the ``mcpFile`` JSON Schema extension keyword.
11671155
1168-
Provides optional hints for client-side file picker filtering and validation.
1169-
All fields are advisory; servers must still validate inputs independently.
1156+
When present on a ``{"type": "string", "format": "uri"}`` property in a
1157+
:class:`Tool` ``input_schema`` or an elicitation ``requested_schema``, it
1158+
marks that property as a file input that clients SHOULD render as a native
1159+
file picker. Selected files are encoded as RFC 2397 data URIs.
1160+
1161+
Both fields are advisory; servers MUST still validate inputs independently.
11701162
"""
11711163

11721164
accept: list[str] | None = None
1173-
"""MIME type patterns the server will accept for this input.
1165+
"""Media type patterns and/or file extensions the client SHOULD filter the
1166+
picker to.
11741167
1175-
Supports exact types (``"image/png"``) and wildcard subtypes (``"image/*"``).
1176-
If omitted, any file type is accepted.
1168+
Supports exact MIME types (``"image/png"``), wildcard subtypes
1169+
(``"image/*"``), and dot-prefixed extensions (``".pdf"``) following the same
1170+
grammar as the HTML ``accept`` attribute. Extension entries are picker hints
1171+
only; server-side validation compares MIME types. If omitted, any file type
1172+
is accepted.
11771173
"""
11781174

11791175
max_size: int | None = None
1180-
"""Maximum file size in bytes (decoded size, per file).
1176+
"""Maximum decoded file size in bytes that the server will accept inline as
1177+
a data URI.
11811178
1182-
Servers should reject larger files with JSON-RPC ``-32602`` (Invalid Params)
1183-
and the structured reason ``"file_too_large"``.
1179+
Servers MUST reject larger payloads with the ``"file_too_large"`` structured
1180+
error reason. For files larger than this, servers obtain the file via
1181+
URL-mode elicitation instead of this property.
11841182
"""
11851183

11861184

@@ -1208,20 +1206,6 @@ class Tool(BaseMetadata):
12081206

12091207
execution: ToolExecution | None = None
12101208

1211-
input_files: dict[str, FileInputDescriptor] | None = None
1212-
"""Declares which arguments in ``input_schema`` are file inputs.
1213-
1214-
Keys must match property names in ``input_schema["properties"]`` and the
1215-
corresponding schema properties must be ``{"type": "string", "format": "uri"}``
1216-
or an array thereof. Servers must not include this field unless the client
1217-
declared the ``file_inputs`` capability during initialization.
1218-
1219-
Clients should render a native file picker for these arguments and encode
1220-
selected files as RFC 2397 data URIs of the form
1221-
``data:<mediatype>;name=<filename>;base64,<data>`` where the ``name=``
1222-
parameter (percent-encoded) carries the original filename.
1223-
"""
1224-
12251209

12261210
class ListToolsResult(PaginatedResult):
12271211
"""The server's response to a tools/list request from the client."""
@@ -1697,20 +1681,6 @@ class ElicitRequestFormParams(RequestParams):
16971681
Only top-level properties are allowed, without nesting.
16981682
"""
16991683

1700-
requested_files: dict[str, FileInputDescriptor] | None = None
1701-
"""Declares which fields in ``requested_schema`` are file inputs.
1702-
1703-
Keys must match property names in ``requested_schema["properties"]`` and the
1704-
corresponding schema properties must be a string schema with ``format: "uri"``
1705-
or an array of such string schemas. Servers must not include this field unless
1706-
the client declared the ``file_inputs`` capability during initialization.
1707-
1708-
Clients should render a native file picker for these fields and encode
1709-
selected files as RFC 2397 data URIs of the form
1710-
``data:<mediatype>;name=<filename>;base64,<data>`` where the ``name=``
1711-
parameter (percent-encoded) carries the original filename.
1712-
"""
1713-
17141684

17151685
class ElicitRequestURLParams(RequestParams):
17161686
"""Parameters for URL mode elicitation requests.

tests/test_types.py

Lines changed: 35 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
CreateMessageResultWithTools,
1111
ElicitRequestFormParams,
1212
FileInputDescriptor,
13-
FileInputsCapability,
1413
Implementation,
1514
InitializeRequest,
1615
InitializeRequestParams,
@@ -366,14 +365,14 @@ def test_list_tools_result_preserves_json_schema_2020_12_fields():
366365

367366

368367
def test_file_input_descriptor_roundtrip():
369-
"""FileInputDescriptor serializes maxSize camelCase and accepts MIME patterns."""
370-
wire: dict[str, Any] = {"accept": ["image/png", "image/*"], "maxSize": 1048576}
368+
"""FileInputDescriptor serializes maxSize camelCase and accepts MIME + extension patterns."""
369+
wire: dict[str, Any] = {"accept": ["image/png", "image/*", ".pdf"], "maxSize": 1048576}
371370
desc = FileInputDescriptor.model_validate(wire)
372-
assert desc.accept == ["image/png", "image/*"]
371+
assert desc.accept == ["image/png", "image/*", ".pdf"]
373372
assert desc.max_size == 1048576
374373

375374
dumped = desc.model_dump(by_alias=True, exclude_none=True)
376-
assert dumped == {"accept": ["image/png", "image/*"], "maxSize": 1048576}
375+
assert dumped == {"accept": ["image/png", "image/*", ".pdf"], "maxSize": 1048576}
377376

378377
# Both fields are optional; empty descriptor is valid
379378
empty = FileInputDescriptor.model_validate({})
@@ -382,89 +381,58 @@ def test_file_input_descriptor_roundtrip():
382381
assert empty.model_dump(by_alias=True, exclude_none=True) == {}
383382

384383

385-
def test_tool_with_input_files():
386-
"""Tool.inputFiles round-trips via wire-format camelCase alias."""
384+
def test_tool_input_schema_mcpfile_roundtrip():
385+
"""mcpFile keyword in Tool.input_schema survives roundtrip and parses as FileInputDescriptor."""
387386
wire: dict[str, Any] = {
388-
"name": "upload_attachment",
389-
"description": "Upload a file",
387+
"name": "describe_image",
388+
"description": "Describe the contents of an image.",
390389
"inputSchema": {
391390
"type": "object",
392391
"properties": {
393-
"file": {"type": "string", "format": "uri"},
394-
"note": {"type": "string"},
392+
"image": {
393+
"type": "string",
394+
"format": "uri",
395+
"mcpFile": {"accept": ["image/png", "image/jpeg"], "maxSize": 5242880},
396+
},
395397
},
396-
"required": ["file"],
397-
},
398-
"inputFiles": {
399-
"file": {"accept": ["application/pdf", "image/*"], "maxSize": 5242880},
398+
"required": ["image"],
400399
},
401400
}
402401
tool = Tool.model_validate(wire)
403-
assert tool.input_files is not None
404-
assert set(tool.input_files.keys()) == {"file"}
405-
assert isinstance(tool.input_files["file"], FileInputDescriptor)
406-
assert tool.input_files["file"].accept == ["application/pdf", "image/*"]
407-
assert tool.input_files["file"].max_size == 5242880
408-
409-
dumped = tool.model_dump(by_alias=True, exclude_none=True)
410-
assert "inputFiles" in dumped
411-
assert "input_files" not in dumped
412-
assert dumped["inputFiles"]["file"]["maxSize"] == 5242880
413-
assert dumped["inputFiles"]["file"]["accept"] == ["application/pdf", "image/*"]
414-
415-
# input_files defaults to None and is omitted when absent
416-
plain = Tool(name="echo", input_schema={"type": "object"})
417-
assert plain.input_files is None
418-
assert "inputFiles" not in plain.model_dump(by_alias=True, exclude_none=True)
402+
image_prop = tool.input_schema["properties"]["image"]
403+
assert image_prop["mcpFile"] == {"accept": ["image/png", "image/jpeg"], "maxSize": 5242880}
419404

405+
desc = FileInputDescriptor.model_validate(image_prop["mcpFile"])
406+
assert desc.accept == ["image/png", "image/jpeg"]
407+
assert desc.max_size == 5242880
420408

421-
def test_client_capabilities_with_file_inputs():
422-
"""ClientCapabilities.fileInputs round-trips as an empty capability object."""
423-
caps = ClientCapabilities.model_validate({"fileInputs": {}})
424-
assert caps.file_inputs is not None
425-
assert isinstance(caps.file_inputs, FileInputsCapability)
426-
427-
dumped = caps.model_dump(by_alias=True, exclude_none=True)
428-
assert dumped == {"fileInputs": {}}
429-
430-
# Absent by default
431-
bare = ClientCapabilities.model_validate({})
432-
assert bare.file_inputs is None
433-
assert "fileInputs" not in bare.model_dump(by_alias=True, exclude_none=True)
409+
dumped = tool.model_dump(by_alias=True, exclude_none=True)
410+
assert dumped["inputSchema"]["properties"]["image"]["mcpFile"]["maxSize"] == 5242880
434411

435412

436-
def test_elicit_form_params_with_requested_files():
437-
"""ElicitRequestFormParams.requestedFiles round-trips through the wire format."""
413+
def test_elicit_form_params_mcpfile_roundtrip():
414+
"""mcpFile keyword in requested_schema survives roundtrip and parses as FileInputDescriptor."""
438415
wire: dict[str, Any] = {
439416
"mode": "form",
440-
"message": "Upload your documents",
417+
"message": "Please select a profile photo.",
441418
"requestedSchema": {
442419
"type": "object",
443420
"properties": {
444-
"resume": {"type": "string", "format": "uri"},
445-
"samples": {
446-
"type": "array",
447-
"items": {"type": "string", "format": "uri"},
448-
"maxItems": 3,
421+
"photo": {
422+
"type": "string",
423+
"format": "uri",
424+
"title": "Profile photo",
425+
"mcpFile": {"accept": ["image/*"], "maxSize": 2097152},
449426
},
450427
},
451-
"required": ["resume"],
452-
},
453-
"requestedFiles": {
454-
"resume": {"accept": ["application/pdf"], "maxSize": 2097152},
455-
"samples": {"accept": ["image/*"]},
428+
"required": ["photo"],
456429
},
457430
}
458431
params = ElicitRequestFormParams.model_validate(wire)
459-
assert params.requested_files is not None
460-
assert isinstance(params.requested_files["resume"], FileInputDescriptor)
461-
assert params.requested_files["resume"].max_size == 2097152
462-
assert params.requested_files["samples"].accept == ["image/*"]
463-
assert params.requested_files["samples"].max_size is None
432+
photo_prop = params.requested_schema["properties"]["photo"]
433+
desc = FileInputDescriptor.model_validate(photo_prop["mcpFile"])
434+
assert desc.accept == ["image/*"]
435+
assert desc.max_size == 2097152
464436

465437
dumped = params.model_dump(by_alias=True, exclude_none=True)
466-
assert "requestedFiles" in dumped
467-
assert "requested_files" not in dumped
468-
assert dumped["requestedFiles"]["resume"]["maxSize"] == 2097152
469-
# samples had no maxSize; ensure it's excluded, not serialized as null
470-
assert "maxSize" not in dumped["requestedFiles"]["samples"]
438+
assert dumped["requestedSchema"]["properties"]["photo"]["mcpFile"]["maxSize"] == 2097152

0 commit comments

Comments
 (0)