-
Notifications
You must be signed in to change notification settings - Fork 3.7k
docs: load media examples from disk instead of inline base64 #3108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,14 @@ | ||
| import base64 | ||
| from pathlib import Path | ||
|
|
||
| from mcp.server import MCPServer | ||
| from mcp.server.mcpserver import Image | ||
|
|
||
| mcp = MCPServer("Brand kit") | ||
|
|
||
| LOGO_PNG = base64.b64decode( | ||
| "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGOQ9bsBAAHPAURf8l/aAAAAAElFTkSuQmCC" | ||
| ) | ||
| LOGO_FILE = Path(__file__).parent / "logo.png" # or the path to your file on disk | ||
|
|
||
|
|
||
| @mcp.tool() | ||
| def logo() -> Image: | ||
| """The brand logo as a PNG.""" | ||
| return Image(data=LOGO_PNG, format="png") | ||
| return Image(path=LOGO_FILE) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,21 @@ | ||
| import base64 | ||
| from pathlib import Path | ||
|
|
||
| from mcp.server import MCPServer | ||
| from mcp.server.mcpserver import Audio, Image | ||
|
|
||
| mcp = MCPServer("Brand kit") | ||
|
|
||
| LOGO_PNG = base64.b64decode( | ||
| "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGOQ9bsBAAHPAURf8l/aAAAAAElFTkSuQmCC" | ||
| ) | ||
|
|
||
| CHIME_WAV = base64.b64decode("UklGRjQAAABXQVZFZm10IBAAAAABAAEAQB8AAIA+AAACABAAZGF0YRAAAAAAAAAAAAAAAAAAAAAAAAAA") | ||
| LOGO_FILE = Path(__file__).parent / "logo.png" | ||
| CHIME_FILE = Path(__file__).parent / "chime.wav" | ||
|
|
||
|
|
||
| @mcp.tool() | ||
| def logo() -> Image: | ||
| """The brand logo as a PNG.""" | ||
| return Image(data=LOGO_PNG, format="png") | ||
| return Image(path=LOGO_FILE) | ||
|
|
||
|
|
||
| @mcp.tool() | ||
| def chime() -> Audio: | ||
| """The notification chime as a WAV.""" | ||
| return Audio(data=CHIME_WAV, format="wav") | ||
| return Audio(path=CHIME_FILE) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,15 @@ | ||
| from mcp_types import Icon | ||
| from pathlib import Path | ||
|
|
||
| from mcp.server import MCPServer | ||
| from mcp.server.mcpserver import Image | ||
|
|
||
| LOGO = Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"]) | ||
| PALETTE = Icon(src="https://example.com/palette.svg", mime_type="image/svg+xml", sizes=["any"]) | ||
| mcp = MCPServer("Brand kit") | ||
|
|
||
| mcp = MCPServer("Brand kit", icons=[LOGO]) | ||
| LOGO_FILE = Path(__file__).parent / "logo.png" | ||
|
|
||
|
|
||
| @mcp.tool(icons=[PALETTE]) | ||
| def palette() -> list[str]: | ||
| """The brand colour palette as hex codes.""" | ||
| return ["#1d4ed8", "#f59e0b", "#10b981"] | ||
|
|
||
|
|
||
| @mcp.resource("brand://guidelines", icons=[LOGO]) | ||
| def guidelines() -> str: | ||
| """How to use the brand assets.""" | ||
| return "Use the primary colour for calls to action." | ||
| @mcp.tool() | ||
| def logo_from_bytes() -> Image: | ||
| """The brand logo as a PNG.""" | ||
| png = LOGO_FILE.read_bytes() # a database read, an HTTP response, Pillow output... | ||
| return Image(data=png, format="png") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| from mcp_types import Icon | ||
|
|
||
| from mcp.server import MCPServer | ||
|
|
||
| LOGO = Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"]) | ||
| PALETTE = Icon(src="https://example.com/palette.svg", mime_type="image/svg+xml", sizes=["any"]) | ||
|
|
||
| mcp = MCPServer("Brand kit", icons=[LOGO]) | ||
|
|
||
|
|
||
| @mcp.tool(icons=[PALETTE]) | ||
| def palette() -> list[str]: | ||
| """The brand colour palette as hex codes.""" | ||
| return ["#1d4ed8", "#f59e0b", "#10b981"] | ||
|
|
||
|
|
||
| @mcp.resource("brand://guidelines", icons=[LOGO]) | ||
| def guidelines() -> str: | ||
| """How to use the brand assets.""" | ||
| return "Use the primary colour for calls to action." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 test_image_result_has_no_structured_content_and_no_output_schema asserts result.structured_content is None without first asserting not result.is_error — and now that this PR makes the logo tool fallible (Image(path=...) does file I/O), an error result also has structured_content=None, so the key assertion passes vacuously on the failure path. Add assert not result.is_error before the structured_content check, matching test_audio_return_becomes_an_audio_content_block added in this same PR.
Extended reasoning...
The bug.
test_image_result_has_no_structured_content_and_no_output_schema(tests/docs_src/test_media.py:41-48) pins the doc claim "media is content for the model, not data for the application" by assertingtool.output_schema is Noneandresult.structured_content is None. It never assertsnot result.is_error. An errorCallToolResultalso hasstructured_content=None, so the structured-content assertion cannot distinguish the documented success behaviour from a failed tool call — it passes on both paths.Why this PR introduced it. Before this PR, the
logotool returned inline bytes (Image(data=LOGO_PNG, format="png")) and could not fail. After it, the tool does file I/O:Image(path=LOGO_FILE)opens the file when the result is built, insideconvert_result. Any exception there is caught byMCPServer._handle_call_tool(src/mcp/server/mcpserver/server.py:412-413), which returnsCallToolResult(content=[TextContent(...)], is_error=True)withstructured_contentdefaulting toNone— it does not raise client-side. The PR touches this exact test (adding@pytest.mark.usefixtures("logo_file")), so the missing guard is squarely in scope.Step-by-step proof of the vacuous pass. Suppose a future edit inlines
Path(__file__).parent / "logo.png"in the tool body while leaving theLOGO_FILEconstant in place (thelogo_filefixture'smonkeypatch.setattr(tutorial001, "LOGO_FILE", path)still succeeds silently — it patches a name nothing reads anymore):client.call_tool("logo", {}).Image(path=...); building the result opens the missing file →FileNotFoundError._handle_call_toolcatches it and returnsCallToolResult(is_error=True, structured_content=None).assert tool.output_schema is None— passes (unrelated to the call).assert result.structured_content is None— passes, because error results carry no structured content.Why nothing else fully prevents it. The sibling
test_image_return_becomes_an_image_content_blockuses the same fixture and does assertnot result.is_errorplus exact content, so a fixture breakage would not be silent at the suite level — that is what keeps this at nit rather than normal. But this individual test's proof is still hollow, and the repo's own bar makes that a defect, not a style preference:.claude/skills/test-quality/SKILL.md's Hollow-proof check requires each claim to be proven by an assertion that cannot pass on an unrelated path, and AGENTS.md mandates conformance to that skill for test work.The fix. One line, matching the pattern the same PR uses in
test_audio_return_becomes_an_audio_content_block: